refactor(analytics): split 365-line analytics into recording + reports
Extract 6 read-only reporting queries (top articles, click trends, popular topics, device breakdown, source performance, CSV export) into Peptide_News_Analytics_Reports (196 lines). Main class retains click recording, daily stats aggregation, IP handling, and device detection (186 lines). Backward-compatible proxies preserved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
196
includes/class-peptide-news-analytics-reports.php
Normal file
196
includes/class-peptide-news-analytics-reports.php
Normal file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
declare( strict_types=1 );
|
||||
/**
|
||||
* Read-only analytics reporting queries.
|
||||
*
|
||||
* Provides aggregated data for the admin analytics dashboard and REST API:
|
||||
* top articles, click trends, popular topics, device breakdown, source
|
||||
* performance, and CSV export.
|
||||
*
|
||||
* Called by: Peptide_News_Rest_API analytics endpoints, admin dashboard.
|
||||
* Dependencies: $wpdb (reads from peptide_news_clicks and peptide_news_daily_stats).
|
||||
*
|
||||
* @since 2.6.0
|
||||
* @see class-peptide-news-analytics.php — Click recording (write path).
|
||||
* @see class-peptide-news-rest-api.php — REST endpoints that consume these queries.
|
||||
*/
|
||||
class Peptide_News_Analytics_Reports {
|
||||
|
||||
/** Hard cap on CSV export rows to prevent memory exhaustion. */
|
||||
const CSV_EXPORT_MAX_ROWS = 50000;
|
||||
|
||||
/**
|
||||
* Get top articles by click count within a date range.
|
||||
*
|
||||
* @param string $start_date Y-m-d.
|
||||
* @param string $end_date Y-m-d.
|
||||
* @param int $limit Max results.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_top_articles( string $start_date, string $end_date, int $limit = 20 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT a.id, a.title, a.source, a.source_url, a.thumbnail_url, a.categories, a.published_at,
|
||||
SUM(s.click_count) AS total_clicks,
|
||||
SUM(s.unique_visitors) AS total_unique
|
||||
FROM {$stats_table} s
|
||||
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
||||
WHERE s.stat_date BETWEEN %s AND %s
|
||||
GROUP BY a.id
|
||||
ORDER BY total_clicks DESC
|
||||
LIMIT %d",
|
||||
$start_date, $end_date, $limit
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get click trends over time (daily totals).
|
||||
*
|
||||
* @param string $start_date Y-m-d.
|
||||
* @param string $end_date Y-m-d.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_click_trends( string $start_date, string $end_date ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT stat_date, SUM(click_count) AS total_clicks, SUM(unique_visitors) AS total_unique
|
||||
FROM {$table}
|
||||
WHERE stat_date BETWEEN %s AND %s
|
||||
GROUP BY stat_date
|
||||
ORDER BY stat_date ASC",
|
||||
$start_date, $end_date
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get popular topics/categories by click volume.
|
||||
*
|
||||
* Aggregates clicks across articles sharing the same category.
|
||||
*
|
||||
* @param string $start_date Y-m-d.
|
||||
* @param string $end_date Y-m-d.
|
||||
* @param int $limit Max topics.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_popular_topics( string $start_date, string $end_date, int $limit = 20 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||
|
||||
$rows = $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT a.categories, a.title, SUM(s.click_count) AS clicks
|
||||
FROM {$stats_table} s
|
||||
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
||||
WHERE s.stat_date BETWEEN %s AND %s AND a.categories != ''
|
||||
GROUP BY a.id
|
||||
ORDER BY clicks DESC",
|
||||
$start_date, $end_date
|
||||
) );
|
||||
|
||||
$topics = array();
|
||||
foreach ( $rows as $row ) {
|
||||
$cats = array_map( 'trim', explode( ',', $row->categories ) );
|
||||
foreach ( $cats as $cat ) {
|
||||
if ( empty( $cat ) ) {
|
||||
continue;
|
||||
}
|
||||
$key = strtolower( $cat );
|
||||
if ( ! isset( $topics[ $key ] ) ) {
|
||||
$topics[ $key ] = array( 'topic' => $cat, 'total_clicks' => 0, 'article_count' => 0 );
|
||||
}
|
||||
$topics[ $key ]['total_clicks'] += (int) $row->clicks;
|
||||
$topics[ $key ]['article_count'] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
usort( $topics, function ( $a, $b ) {
|
||||
return $b['total_clicks'] - $a['total_clicks'];
|
||||
} );
|
||||
|
||||
return array_slice( $topics, 0, $limit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device breakdown stats.
|
||||
*
|
||||
* @param string $start_date Y-m-d.
|
||||
* @param string $end_date Y-m-d.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_device_breakdown( string $start_date, string $end_date ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'peptide_news_clicks';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT device_type, COUNT(*) AS click_count
|
||||
FROM {$table}
|
||||
WHERE DATE(clicked_at) BETWEEN %s AND %s
|
||||
GROUP BY device_type
|
||||
ORDER BY click_count DESC",
|
||||
$start_date, $end_date
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source performance stats.
|
||||
*
|
||||
* @param string $start_date Y-m-d.
|
||||
* @param string $end_date Y-m-d.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_source_performance( string $start_date, string $end_date ): array {
|
||||
global $wpdb;
|
||||
|
||||
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT a.source,
|
||||
COUNT(DISTINCT a.id) AS article_count,
|
||||
SUM(s.click_count) AS total_clicks,
|
||||
ROUND(IFNULL(SUM(s.click_count) / NULLIF(COUNT(DISTINCT a.id), 0), 0), 1) AS avg_clicks_per_article
|
||||
FROM {$stats_table} s
|
||||
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
||||
WHERE s.stat_date BETWEEN %s AND %s
|
||||
GROUP BY a.source
|
||||
ORDER BY total_clicks DESC",
|
||||
$start_date, $end_date
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Export click data as CSV-ready array.
|
||||
*
|
||||
* Capped at CSV_EXPORT_MAX_ROWS at the SQL level.
|
||||
*
|
||||
* @param string $start_date Y-m-d.
|
||||
* @param string $end_date Y-m-d.
|
||||
* @return array Associative arrays (one per click row).
|
||||
*/
|
||||
public static function export_clicks_csv( string $start_date, string $end_date ): array {
|
||||
global $wpdb;
|
||||
|
||||
$clicks_table = $wpdb->prefix . 'peptide_news_clicks';
|
||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT c.clicked_at, a.title, a.source, a.source_url, a.categories,
|
||||
c.referrer_url, c.page_url, c.device_type, c.session_id
|
||||
FROM {$clicks_table} c
|
||||
INNER JOIN {$articles_table} a ON a.id = c.article_id
|
||||
WHERE DATE(c.clicked_at) BETWEEN %s AND %s
|
||||
ORDER BY c.clicked_at DESC
|
||||
LIMIT %d",
|
||||
$start_date, $end_date, self::CSV_EXPORT_MAX_ROWS
|
||||
), ARRAY_A );
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,59 @@
|
||||
<?php
|
||||
declare( strict_types=1 );
|
||||
/**
|
||||
* Click analytics tracking and reporting.
|
||||
* Click analytics — recording and aggregation.
|
||||
*
|
||||
* Handles recording click events, aggregating daily stats,
|
||||
* and generating trend/topic reports.
|
||||
* Handles recording click events from the frontend, updating daily
|
||||
* aggregate stats, and providing utility methods for IP handling
|
||||
* and device detection.
|
||||
*
|
||||
* Called by: Peptide_News_Public::handle_click_tracking() (AJAX endpoint).
|
||||
* Dependencies: $wpdb (writes to peptide_news_clicks and peptide_news_daily_stats).
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @see class-peptide-news-analytics-reports.php — Read-only reporting queries.
|
||||
* @see class-peptide-news-rest-api.php — REST endpoints for analytics data.
|
||||
*/
|
||||
class Peptide_News_Analytics {
|
||||
|
||||
/**
|
||||
* Record a click event for an article.
|
||||
*
|
||||
* @param int $article_id
|
||||
* @param array $meta Additional context (IP, UA, referrer, etc.).
|
||||
* @return bool
|
||||
* Side effects: DB insert to clicks table, daily stats upsert.
|
||||
*
|
||||
* @param int $article_id Article ID that was clicked.
|
||||
* @param array $meta Context: ip, user_agent, referrer, page_url, session_id.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public static function record_click( int $article_id, array $meta = array() ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'peptide_news_clicks';
|
||||
$ip = isset( $meta['ip'] ) ? $meta['ip'] : self::get_client_ip();
|
||||
|
||||
$ip = isset( $meta['ip'] ) ? $meta['ip'] : self::get_client_ip();
|
||||
|
||||
// Skip recording if no valid IP is available.
|
||||
if ( null === $ip ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Optionally anonymize IP (GDPR-friendly).
|
||||
// GDPR-friendly IP anonymization.
|
||||
if ( get_option( 'peptide_news_anonymize_ip', 1 ) ) {
|
||||
$ip = self::anonymize_ip( $ip );
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'article_id' => absint( $article_id ),
|
||||
'clicked_at' => current_time( 'mysql' ),
|
||||
'user_ip' => sanitize_text_field( $ip ),
|
||||
'user_agent' => sanitize_text_field( isset( $meta['user_agent'] ) ? $meta['user_agent'] : '' ),
|
||||
'referrer_url' => esc_url_raw( isset( $meta['referrer'] ) ? $meta['referrer'] : '' ),
|
||||
'page_url' => esc_url_raw( isset( $meta['page_url'] ) ? $meta['page_url'] : '' ),
|
||||
'session_id' => sanitize_text_field( isset( $meta['session_id'] ) ? $meta['session_id'] : '' ),
|
||||
'user_id' => is_user_logged_in() ? get_current_user_id() : null,
|
||||
'device_type' => sanitize_text_field( self::detect_device( isset( $meta['user_agent'] ) ? $meta['user_agent'] : '' ) ),
|
||||
'article_id' => absint( $article_id ),
|
||||
'clicked_at' => current_time( 'mysql' ),
|
||||
'user_ip' => sanitize_text_field( $ip ),
|
||||
'user_agent' => sanitize_text_field( $meta['user_agent'] ?? '' ),
|
||||
'referrer_url' => esc_url_raw( $meta['referrer'] ?? '' ),
|
||||
'page_url' => esc_url_raw( $meta['page_url'] ?? '' ),
|
||||
'session_id' => sanitize_text_field( $meta['session_id'] ?? '' ),
|
||||
'user_id' => is_user_logged_in() ? get_current_user_id() : null,
|
||||
'device_type' => sanitize_text_field( self::detect_device( $meta['user_agent'] ?? '' ) ),
|
||||
);
|
||||
|
||||
$result = $wpdb->insert( $table, $data );
|
||||
|
||||
// Update daily aggregate.
|
||||
if ( false !== $result ) {
|
||||
self::update_daily_stats( $article_id, $ip );
|
||||
}
|
||||
@@ -57,265 +62,59 @@ class Peptide_News_Analytics {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update daily aggregated stats.
|
||||
* Upsert daily aggregated stats for an article.
|
||||
*
|
||||
* @param int $article_id
|
||||
* @param string $ip Anonymized IP for unique visitor counting.
|
||||
* @param int $article_id Article ID.
|
||||
* @param string $ip Anonymized IP for unique visitor counting.
|
||||
*/
|
||||
private static function update_daily_stats( int $article_id, string $ip ): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
$today = current_time( 'Y-m-d' );
|
||||
$table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
$today = current_time( 'Y-m-d' );
|
||||
$clicks_tbl = $wpdb->prefix . 'peptide_news_clicks';
|
||||
|
||||
// Upsert daily click count.
|
||||
$existing = $wpdb->get_row( $wpdb->prepare(
|
||||
"SELECT id, click_count FROM {$table} WHERE article_id = %d AND stat_date = %s",
|
||||
$article_id,
|
||||
$today
|
||||
$article_id, $today
|
||||
) );
|
||||
|
||||
// Count unique IPs for today.
|
||||
$unique = $wpdb->get_var( $wpdb->prepare(
|
||||
"SELECT COUNT(DISTINCT user_ip) FROM {$clicks_tbl} WHERE article_id = %d AND DATE(clicked_at) = %s",
|
||||
$article_id,
|
||||
$today
|
||||
$article_id, $today
|
||||
) );
|
||||
|
||||
if ( $existing ) {
|
||||
$wpdb->update(
|
||||
$table,
|
||||
array(
|
||||
'click_count' => $existing->click_count + 1,
|
||||
'unique_visitors' => (int) $unique,
|
||||
),
|
||||
array( 'click_count' => $existing->click_count + 1, 'unique_visitors' => (int) $unique ),
|
||||
array( 'id' => $existing->id ),
|
||||
array( '%d', '%d' ),
|
||||
array( '%d' )
|
||||
);
|
||||
} else {
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'article_id' => $article_id,
|
||||
'stat_date' => $today,
|
||||
'click_count' => 1,
|
||||
'unique_visitors' => 1,
|
||||
)
|
||||
);
|
||||
$wpdb->insert( $table, array(
|
||||
'article_id' => $article_id,
|
||||
'stat_date' => $today,
|
||||
'click_count' => 1,
|
||||
'unique_visitors' => 1,
|
||||
) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top articles by click count within a date range.
|
||||
* Get the client IP address from server headers.
|
||||
*
|
||||
* @param string $start_date Y-m-d
|
||||
* @param string $end_date Y-m-d
|
||||
* @param int $limit
|
||||
* @return array
|
||||
*/
|
||||
public static function get_top_articles( string $start_date, string $end_date, int $limit = 20 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT a.id, a.title, a.source, a.source_url, a.thumbnail_url, a.categories, a.published_at,
|
||||
SUM(s.click_count) AS total_clicks,
|
||||
SUM(s.unique_visitors) AS total_unique
|
||||
FROM {$stats_table} s
|
||||
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
||||
WHERE s.stat_date BETWEEN %s AND %s
|
||||
GROUP BY a.id
|
||||
ORDER BY total_clicks DESC
|
||||
LIMIT %d",
|
||||
$start_date,
|
||||
$end_date,
|
||||
$limit
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get click trends over time (daily totals).
|
||||
* Checks CF-Connecting-IP, X-Forwarded-For, X-Real-IP, then REMOTE_ADDR.
|
||||
*
|
||||
* @param string $start_date
|
||||
* @param string $end_date
|
||||
* @return array
|
||||
*/
|
||||
public static function get_click_trends( string $start_date, string $end_date ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT stat_date, SUM(click_count) AS total_clicks, SUM(unique_visitors) AS total_unique
|
||||
FROM {$table}
|
||||
WHERE stat_date BETWEEN %s AND %s
|
||||
GROUP BY stat_date
|
||||
ORDER BY stat_date ASC",
|
||||
$start_date,
|
||||
$end_date
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get popular topics/categories by click volume.
|
||||
*
|
||||
* @param string $start_date
|
||||
* @param string $end_date
|
||||
* @param int $limit
|
||||
* @return array
|
||||
*/
|
||||
public static function get_popular_topics( string $start_date, string $end_date, int $limit = 20 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||
|
||||
// Get articles with their click counts.
|
||||
$rows = $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT a.categories, a.title, SUM(s.click_count) AS clicks
|
||||
FROM {$stats_table} s
|
||||
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
||||
WHERE s.stat_date BETWEEN %s AND %s AND a.categories != ''
|
||||
GROUP BY a.id
|
||||
ORDER BY clicks DESC",
|
||||
$start_date,
|
||||
$end_date
|
||||
) );
|
||||
|
||||
// Aggregate by category.
|
||||
$topics = array();
|
||||
foreach ( $rows as $row ) {
|
||||
$cats = array_map( 'trim', explode( ',', $row->categories ) );
|
||||
foreach ( $cats as $cat ) {
|
||||
if ( empty( $cat ) ) {
|
||||
continue;
|
||||
}
|
||||
$cat_lower = strtolower( $cat );
|
||||
if ( ! isset( $topics[ $cat_lower ] ) ) {
|
||||
$topics[ $cat_lower ] = array(
|
||||
'topic' => $cat,
|
||||
'total_clicks' => 0,
|
||||
'article_count' => 0,
|
||||
);
|
||||
}
|
||||
$topics[ $cat_lower ]['total_clicks'] += (int) $row->clicks;
|
||||
$topics[ $cat_lower ]['article_count'] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by clicks.
|
||||
usort( $topics, function ( $a, $b ) {
|
||||
return $b['total_clicks'] - $a['total_clicks'];
|
||||
} );
|
||||
|
||||
return array_slice( $topics, 0, $limit );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device breakdown stats.
|
||||
*
|
||||
* @param string $start_date
|
||||
* @param string $end_date
|
||||
* @return array
|
||||
*/
|
||||
public static function get_device_breakdown( string $start_date, string $end_date ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'peptide_news_clicks';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT device_type, COUNT(*) AS click_count
|
||||
FROM {$table}
|
||||
WHERE DATE(clicked_at) BETWEEN %s AND %s
|
||||
GROUP BY device_type
|
||||
ORDER BY click_count DESC",
|
||||
$start_date,
|
||||
$end_date
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source performance stats.
|
||||
*
|
||||
* @param string $start_date
|
||||
* @param string $end_date
|
||||
* @return array
|
||||
*/
|
||||
public static function get_source_performance( string $start_date, string $end_date ): array {
|
||||
global $wpdb;
|
||||
|
||||
$stats_table = $wpdb->prefix . 'peptide_news_daily_stats';
|
||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT a.source,
|
||||
COUNT(DISTINCT a.id) AS article_count,
|
||||
SUM(s.click_count) AS total_clicks,
|
||||
ROUND(IFNULL(SUM(s.click_count) / NULLIF(COUNT(DISTINCT a.id), 0), 0), 1) AS avg_clicks_per_article
|
||||
FROM {$stats_table} s
|
||||
INNER JOIN {$articles_table} a ON a.id = s.article_id
|
||||
WHERE s.stat_date BETWEEN %s AND %s
|
||||
GROUP BY a.source
|
||||
ORDER BY total_clicks DESC",
|
||||
$start_date,
|
||||
$end_date
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard cap on CSV export rows to prevent memory exhaustion on large datasets.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const CSV_EXPORT_MAX_ROWS = 50000;
|
||||
|
||||
/**
|
||||
* Export click data as CSV-ready array.
|
||||
*
|
||||
* Capped at CSV_EXPORT_MAX_ROWS to prevent memory exhaustion on sites
|
||||
* with large click volumes. The cap is applied at the SQL level so
|
||||
* only the limited result set is ever loaded into PHP memory.
|
||||
*
|
||||
* @param string $start_date Y-m-d format.
|
||||
* @param string $end_date Y-m-d format.
|
||||
* @return array Array of associative arrays (one per click row).
|
||||
*/
|
||||
public static function export_clicks_csv( string $start_date, string $end_date ): array {
|
||||
global $wpdb;
|
||||
|
||||
$clicks_table = $wpdb->prefix . 'peptide_news_clicks';
|
||||
$articles_table = $wpdb->prefix . 'peptide_news_articles';
|
||||
|
||||
return $wpdb->get_results( $wpdb->prepare(
|
||||
"SELECT c.clicked_at, a.title, a.source, a.source_url, a.categories,
|
||||
c.referrer_url, c.page_url, c.device_type, c.session_id
|
||||
FROM {$clicks_table} c
|
||||
INNER JOIN {$articles_table} a ON a.id = c.article_id
|
||||
WHERE DATE(c.clicked_at) BETWEEN %s AND %s
|
||||
ORDER BY c.clicked_at DESC
|
||||
LIMIT %d",
|
||||
$start_date,
|
||||
$end_date,
|
||||
self::CSV_EXPORT_MAX_ROWS
|
||||
), ARRAY_A );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the client IP address.
|
||||
*
|
||||
* @return string|null
|
||||
* @return string|null Valid IP or null.
|
||||
*/
|
||||
private static function get_client_ip(): ?string {
|
||||
$ip_keys = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR' );
|
||||
|
||||
foreach ( $ip_keys as $key ) {
|
||||
if ( ! empty( $_SERVER[ $key ] ) ) {
|
||||
$ip = explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) );
|
||||
$ip = trim( $ip[0] );
|
||||
$ip = trim( explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) )[0] );
|
||||
if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
|
||||
return $ip;
|
||||
}
|
||||
@@ -325,12 +124,7 @@ class Peptide_News_Analytics {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Anonymize an IP address (zero out last octet for IPv4, last 80 bits for IPv6).
|
||||
*
|
||||
* @param string $ip
|
||||
* @return string
|
||||
*/
|
||||
/** Anonymize IP: zero last octet (IPv4) or last 80 bits (IPv6). */
|
||||
private static function anonymize_ip( string $ip ): string {
|
||||
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
|
||||
return preg_replace( '/\.\d+$/', '.0', $ip );
|
||||
@@ -341,12 +135,7 @@ class Peptide_News_Analytics {
|
||||
return '0.0.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple device detection from user agent.
|
||||
*
|
||||
* @param string $ua
|
||||
* @return string
|
||||
*/
|
||||
/** Simple device detection from user agent string. */
|
||||
private static function detect_device( string $ua ): string {
|
||||
$ua = strtolower( $ua );
|
||||
|
||||
@@ -362,4 +151,36 @@ class Peptide_News_Analytics {
|
||||
|
||||
return 'desktop';
|
||||
}
|
||||
|
||||
// ── Backward-compatible proxies ─────────────────────────────────────
|
||||
|
||||
/** @see Peptide_News_Analytics_Reports::get_top_articles() */
|
||||
public static function get_top_articles( string $start_date, string $end_date, int $limit = 20 ): array {
|
||||
return Peptide_News_Analytics_Reports::get_top_articles( $start_date, $end_date, $limit );
|
||||
}
|
||||
|
||||
/** @see Peptide_News_Analytics_Reports::get_click_trends() */
|
||||
public static function get_click_trends( string $start_date, string $end_date ): array {
|
||||
return Peptide_News_Analytics_Reports::get_click_trends( $start_date, $end_date );
|
||||
}
|
||||
|
||||
/** @see Peptide_News_Analytics_Reports::get_popular_topics() */
|
||||
public static function get_popular_topics( string $start_date, string $end_date, int $limit = 20 ): array {
|
||||
return Peptide_News_Analytics_Reports::get_popular_topics( $start_date, $end_date, $limit );
|
||||
}
|
||||
|
||||
/** @see Peptide_News_Analytics_Reports::get_device_breakdown() */
|
||||
public static function get_device_breakdown( string $start_date, string $end_date ): array {
|
||||
return Peptide_News_Analytics_Reports::get_device_breakdown( $start_date, $end_date );
|
||||
}
|
||||
|
||||
/** @see Peptide_News_Analytics_Reports::get_source_performance() */
|
||||
public static function get_source_performance( string $start_date, string $end_date ): array {
|
||||
return Peptide_News_Analytics_Reports::get_source_performance( $start_date, $end_date );
|
||||
}
|
||||
|
||||
/** @see Peptide_News_Analytics_Reports::export_clicks_csv() */
|
||||
public static function export_clicks_csv( string $start_date, string $end_date ): array {
|
||||
return Peptide_News_Analytics_Reports::export_clicks_csv( $start_date, $end_date );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class Peptide_News {
|
||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-source-resolver.php';
|
||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-rss-source.php';
|
||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-fetcher.php';
|
||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-analytics-reports.php';
|
||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-analytics.php';
|
||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-rest-articles.php';
|
||||
require_once PEPTIDE_NEWS_PLUGIN_DIR . 'includes/class-peptide-news-rest-api.php';
|
||||
|
||||
Reference in New Issue
Block a user