<?php
|
namespace JVBase\integrations;
|
|
use JVBase\managers\Cache;
|
use WP_Error;
|
use WP_Post;
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class Umami extends Integrations
|
{
|
private string $website_id;
|
private string $api_url;
|
private string $api_token;
|
|
// Tracking configuration
|
private array $valid_events = [
|
'view_feed',
|
'view_taxonomy',
|
'view_profile',
|
'view_shop',
|
'view_content',
|
'toggle_favourite',
|
'click_profile',
|
'click_content',
|
'click_taxonomy',
|
'click_shop',
|
'user_session'
|
];
|
|
// Database tables
|
private string $events_table;
|
private string $metrics_table;
|
|
public function __construct(?int $userID = null)
|
{
|
$this->service_name = 'umami';
|
$this->title = 'Umami.js';
|
$this->icon = 'chart-line';
|
$this->apiBase = ''; // Set dynamically based on credentials
|
$this->apiEndpoints = [
|
'api/websites',
|
'api/websites/{websiteId}/stats',
|
'api/websites/{websiteId}/pageviews',
|
'api/websites/{websiteId}/events'
|
];
|
|
$this->fields = [
|
'website_id' => [
|
'label' => 'Website ID',
|
'type' => 'text',
|
'placeholder'=> 'Enter Umami Website ID',
|
'hint' => 'Found in your Umami dashboard under Settings → Websites.',
|
'required' => true
|
],
|
];
|
|
$this->advanced = [
|
'api_url' => [
|
'label' => 'API URL',
|
'type' => 'text',
|
'placeholder'=> 'https://analytics.yourdomain.com',
|
'hint' => 'Your Umami server URL. Leave blank if using umami.is cloud service.'
|
],
|
'api_token' => [
|
'label' => 'API Token (Optional)',
|
'type' => 'text',
|
'subtype' => 'password',
|
'placeholder'=> 'Enter API Token for analytics data',
|
'hint' => 'Required only if you want to retrieve analytics data for dashboards.'
|
]
|
];
|
|
$this->instructions = [
|
'Login to your <a href="https://cloud.umami.is/settings/websites" target="_blank">umami.is</a> account, select your website, and copy the website ID'
|
];
|
|
parent::__construct($userID);
|
|
$this->actions = array_merge(
|
$this->actions,
|
[
|
'refresh_data' => 'handleRefreshData'
|
]
|
);
|
|
global $wpdb;
|
$this->events_table = $wpdb->prefix . BASE . 'umami_events';
|
$this->metrics_table = $wpdb->prefix . BASE . 'performance_metrics';
|
}
|
|
/**
|
* Initialize service-specific properties
|
*/
|
protected function initialize(): void
|
{
|
$this->website_id = $this->credentials['website_id'] ?? '';
|
$this->api_url = $this->credentials['api_url'] ?? '';
|
$this->api_token = $this->credentials['api_token'] ?? '';
|
$this->ttl = intval($this->credentials['cache_duration'] ?? 60) * 60;
|
|
// Set dynamic API base if provided
|
if (!empty($this->api_url)) {
|
$this->apiBase = rtrim($this->api_url, '/');
|
}
|
}
|
|
/**
|
* Register additional WordPress hooks
|
*/
|
protected function registerAdditionalHooks(): void
|
{
|
// Add tracking script to frontend
|
add_action('wp_head', [$this, 'renderTrackingScript'], 5);
|
|
// Add user session tracking
|
add_action('wp_footer', [$this, 'trackUserSession'], 100);
|
|
// Schedule data collection if API is configured
|
if (!empty($this->api_token)) {
|
add_action('jvb_daily_umami_collection', [$this, 'collectDailyData']);
|
|
if (!wp_next_scheduled('jvb_daily_umami_collection')) {
|
wp_schedule_event(time(), 'daily', 'jvb_daily_umami_collection');
|
}
|
}
|
}
|
|
/***************************************************************************
|
* TRACKING SCRIPT METHODS
|
***************************************************************************/
|
|
/**
|
* Render tracking script in head
|
*/
|
public function renderTrackingScript(): void
|
{
|
// Skip on local environments
|
if (JVB_TESTING) {
|
return;
|
}
|
if (!$this->isSetUp() || is_admin()) {
|
return;
|
}
|
|
|
$script_url = $this->getTrackingScriptUrl();
|
$website_id = $this->getWebsiteId();
|
|
if (!$website_id || !$script_url) {
|
return;
|
}
|
|
// Preconnect for performance
|
$domain = parse_url($script_url, PHP_URL_HOST);
|
echo '<link rel="preconnect" href="https://' . esc_attr($domain) . '"/>' . "\n";
|
|
// Add tracking script
|
$attributes = [
|
'defer',
|
'src="' . esc_url($script_url) . '"',
|
'data-website-id="' . esc_attr($website_id) . '"'
|
];
|
|
// Add optional configuration
|
if (!empty($this->credentials['respect_dnt']) && $this->credentials['respect_dnt']) {
|
$attributes[] = 'data-do-not-track="true"';
|
}
|
|
if (!empty($this->credentials['domains'])) {
|
$attributes[] = 'data-domains="' . esc_attr($this->credentials['domains']) . '"';
|
}
|
|
echo '<script ' . implode(' ', $attributes) . '></script>' . "\n";
|
}
|
|
/**
|
* Track user session information
|
*/
|
public function trackUserSession(): void
|
{
|
if (!$this->isSetUp() || is_admin()) {
|
return;
|
}
|
|
$attributes = $this->buildTrackingAttributes('user_session', 'pageview', [
|
'logged_in' => is_user_logged_in() ? 'true' : 'false',
|
'user_type' => $this->getCurrentUserType()
|
]);
|
|
if (empty($attributes)) {
|
return;
|
}
|
|
echo '<div ' . $this->attributesToString($attributes) . ' style="display:none"></div>' . "\n";
|
|
// Add page-specific tracking
|
$this->trackPageView();
|
}
|
|
/**
|
* Track specific page views
|
*/
|
private function trackPageView(): void
|
{
|
$tracker_args = [];
|
$event = '';
|
$type = '';
|
|
if (is_singular([BASE . 'artist', BASE . 'partner'])) {
|
// Profile view
|
global $post;
|
$event = 'view_profile';
|
$type = get_post_type();
|
$tracker_args = [
|
'id' => get_the_ID(),
|
'from' => $this->getTrafficSource(),
|
'owner_id' => $post->post_author
|
];
|
|
// Check for specific content in query
|
foreach (['tattoo', 'piercing', 'artwork'] as $content_type) {
|
if (isset($_GET[$content_type])) {
|
$tracker_args['item'] = sanitize_text_field($_GET[$content_type]);
|
break;
|
}
|
}
|
|
} elseif (is_tax(BASE . 'shop')) {
|
// Shop view
|
$event = 'view_shop';
|
$type = BASE . 'shop';
|
$tracker_args = [
|
'id' => get_queried_object_id(),
|
'from' => $this->getTrafficSource()
|
];
|
|
} elseif (is_tax()) {
|
// Taxonomy view
|
$obj = get_queried_object();
|
$event = 'view_taxonomy';
|
$type = $obj->taxonomy;
|
$tracker_args = [
|
'id' => $obj->term_id,
|
'from' => $this->getTrafficSource()
|
];
|
}
|
|
if ($event && $type) {
|
$attributes = $this->buildTrackingAttributes($event, $type, $tracker_args);
|
echo '<div ' . $this->attributesToString($attributes) . ' style="display:none"></div>' . "\n";
|
}
|
}
|
|
/***************************************************************************
|
* TRACKING ATTRIBUTE BUILDER METHODS
|
***************************************************************************/
|
|
/**
|
* Build tracking attributes for events
|
*/
|
public function buildTrackingAttributes(string $event, string $type, array $args = []): array
|
{
|
if (!in_array($event, $this->valid_events)) {
|
return [];
|
}
|
|
// Normalize type
|
$normalized_type = str_replace(BASE, '', $type);
|
|
// Build base attributes
|
$attributes = [
|
'data-umami-event' => esc_attr($event),
|
'data-umami-event-type' => esc_attr($normalized_type)
|
];
|
|
// Add ID if provided
|
if (!empty($args['id'])) {
|
$attributes['data-umami-event-id'] = esc_attr($args['id']);
|
}
|
|
// Add source information
|
if (!empty($args['source_id'])) {
|
$attributes['data-umami-event-source'] = esc_attr($args['source_id']);
|
if (!empty($args['source_type'])) {
|
$attributes['data-umami-event-source-type'] = esc_attr(
|
str_replace(BASE, '', $args['source_type'])
|
);
|
}
|
}
|
|
// Add owner information
|
if (!empty($args['owner_id'])) {
|
$attributes['data-umami-event-owner'] = esc_attr($args['owner_id']);
|
if (!empty($args['owner_type'])) {
|
$attributes['data-umami-event-owner-type'] = esc_attr(
|
str_replace(BASE, '', $args['owner_type'])
|
);
|
}
|
}
|
|
// Add traffic source
|
if (!empty($args['from'])) {
|
$attributes['data-umami-event-from'] = esc_attr($args['from']);
|
}
|
|
// Add item reference
|
if (!empty($args['item'])) {
|
$attributes['data-umami-event-item'] = esc_attr($args['item']);
|
}
|
|
// Add metadata
|
foreach ($args as $key => $value) {
|
if (in_array($key, ['id', 'source_id', 'source_type', 'owner_id', 'owner_type', 'from', 'item'])) {
|
continue;
|
}
|
$clean_key = 'data-umami-event-' . str_replace('_', '-', sanitize_key($key));
|
$attributes[$clean_key] = esc_attr($value);
|
}
|
|
return $attributes;
|
}
|
|
/**
|
* Convert attributes array to HTML string
|
*/
|
public function attributesToString(array $attributes): string
|
{
|
$attr_strings = [];
|
foreach ($attributes as $key => $value) {
|
$attr_strings[] = $key . '="' . $value . '"';
|
}
|
return implode(' ', $attr_strings);
|
}
|
|
/**
|
* Public tracking methods for use in templates
|
*/
|
public function trackContentClick(int $id, string $type, array $args = []): string
|
{
|
$args['id'] = $id;
|
|
// Auto-detect owner for content types
|
if (empty($args['owner_id']) && in_array($type, [BASE . 'tattoo', BASE . 'artwork', BASE . 'piercing'])) {
|
$post = get_post($id);
|
if ($post && !empty($post->post_author)) {
|
$args['owner_id'] = $post->post_author;
|
$args['owner_type'] = 'user';
|
}
|
}
|
|
return $this->attributesToString(
|
$this->buildTrackingAttributes('click_content', $type, $args)
|
);
|
}
|
|
public function trackProfileClick(int $id, string $type, array $args = []): string
|
{
|
$args['id'] = $id;
|
return $this->attributesToString(
|
$this->buildTrackingAttributes('click_profile', $type, $args)
|
);
|
}
|
|
public function trackTaxonomyClick(int $id, string $taxonomy, array $args = []): string
|
{
|
$args['id'] = $id;
|
return $this->attributesToString(
|
$this->buildTrackingAttributes('click_taxonomy', $taxonomy, $args)
|
);
|
}
|
|
public function trackContentTaxonomyClick(int $id, string $taxonomy, array $args = []): string
|
{
|
$args['id'] = $id;
|
return $this->attributesToString(
|
$this->buildTrackingAttributes('click_' . $taxonomy, $taxonomy, $args)
|
);
|
}
|
|
public function trackFavouriteToggle(int $id, string $type, bool $is_favourite, array $args = []): string
|
{
|
$args['id'] = $id;
|
$args['action'] = $is_favourite ? 'add' : 'remove';
|
return $this->attributesToString(
|
$this->buildTrackingAttributes('toggle_favourite', $type, $args)
|
);
|
}
|
|
public function trackFeedView(int $id, string $type, array $args = []): array
|
{
|
$args['id'] = $id;
|
return $this->buildTrackingAttributes('view_feed', $type, $args);
|
}
|
|
/***************************************************************************
|
* API DATA COLLECTION METHODS
|
***************************************************************************/
|
|
/**
|
* Get analytics data from Umami API
|
*/
|
public function getAnalytics(string $start_date, string $end_date, bool $use_cache = true): ?array
|
{
|
if (empty($this->api_url) || empty($this->api_token)) {
|
return null;
|
}
|
|
// Check cache first
|
if ($use_cache && $this->cache) {
|
$cache_key = md5("analytics_{$start_date}_{$end_date}");
|
$cached = $this->cache->get($cache_key);
|
if ($cached !== false) {
|
return $cached;
|
}
|
}
|
|
try {
|
$url = rtrim($this->api_url, '/') . "/api/websites/{$this->website_id}/stats";
|
$params = [
|
'startAt' => strtotime($start_date) * 1000,
|
'endAt' => strtotime($end_date) * 1000
|
];
|
|
$response = wp_remote_get($url . '?' . http_build_query($params), [
|
'headers' => [
|
'Authorization' => 'Bearer ' . $this->api_token,
|
'Accept' => 'application/json'
|
],
|
'timeout' => 30
|
]);
|
|
if (is_wp_error($response)) {
|
$this->logError('API request failed', ['error' => $response->get_error_message()]);
|
return null;
|
}
|
|
$body = wp_remote_retrieve_body($response);
|
$data = json_decode($body, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
$this->logError('Invalid JSON response from API');
|
return null;
|
}
|
|
// Cache the result
|
if ($use_cache && $this->cache) {
|
$this->cache->set($cache_key, $data, $this->ttl);
|
}
|
|
return $data;
|
|
} catch (Exception $e) {
|
$this->logError('Exception during API call', ['error' => $e->getMessage()]);
|
return null;
|
}
|
}
|
|
/**
|
* Get page views for a specific URL
|
*/
|
public function getPageViews(string $url, string $start_date, string $end_date): ?array
|
{
|
if (empty($this->api_url) || empty($this->api_token)) {
|
return null;
|
}
|
|
$api_endpoint = rtrim($this->api_url, '/') . "/api/websites/{$this->website_id}/pageviews";
|
$params = [
|
'startAt' => strtotime($start_date) * 1000,
|
'endAt' => strtotime($end_date) * 1000,
|
'url' => $url,
|
'unit' => 'day'
|
];
|
|
$response = wp_remote_get($api_endpoint . '?' . http_build_query($params), [
|
'headers' => [
|
'Authorization' => 'Bearer ' . $this->api_token,
|
'Accept' => 'application/json'
|
],
|
'timeout' => 30
|
]);
|
|
if (is_wp_error($response)) {
|
return null;
|
}
|
|
return json_decode(wp_remote_retrieve_body($response), true);
|
}
|
|
/**
|
* Get custom events data
|
*/
|
public function getEvents(string $event_name = '', string $start_date = '', string $end_date = ''): ?array
|
{
|
if (empty($this->api_url) || empty($this->api_token)) {
|
return null;
|
}
|
|
$api_endpoint = rtrim($this->api_url, '/') . "/api/websites/{$this->website_id}/events";
|
|
$params = [];
|
if ($start_date && $end_date) {
|
$params['startAt'] = strtotime($start_date) * 1000;
|
$params['endAt'] = strtotime($end_date) * 1000;
|
}
|
if ($event_name) {
|
$params['event'] = $event_name;
|
}
|
|
$url = $api_endpoint;
|
if (!empty($params)) {
|
$url .= '?' . http_build_query($params);
|
}
|
|
$response = wp_remote_get($url, [
|
'headers' => [
|
'Authorization' => 'Bearer ' . $this->api_token,
|
'Accept' => 'application/json'
|
],
|
'timeout' => 30
|
]);
|
|
if (is_wp_error($response)) {
|
return null;
|
}
|
|
return json_decode(wp_remote_retrieve_body($response), true);
|
}
|
|
/**
|
* Collect daily analytics data and store in database
|
*/
|
public function collectDailyData(): void
|
{
|
if (!$this->isSetUp() || empty($this->api_token)) {
|
return;
|
}
|
|
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
$data = $this->getAnalytics($yesterday, $yesterday, false);
|
|
if (!$data) {
|
$this->logError('Failed to collect daily data', ['date' => $yesterday]);
|
return;
|
}
|
|
global $wpdb;
|
|
// Store basic metrics
|
$wpdb->insert($this->metrics_table, [
|
'date' => $yesterday,
|
'pageviews' => $data['pageviews']['value'] ?? 0,
|
'visitors' => $data['visitors']['value'] ?? 0,
|
'visits' => $data['visits']['value'] ?? 0,
|
'bounces' => $data['bounces']['value'] ?? 0,
|
'total_time' => $data['totaltime']['value'] ?? 0,
|
'created_at' => current_time('mysql')
|
]);
|
|
// Collect and store events
|
$events = $this->getEvents('', $yesterday, $yesterday);
|
if ($events) {
|
foreach ($events as $event) {
|
$wpdb->insert($this->events_table, [
|
'date' => $yesterday,
|
'event_name' => $event['x'] ?? '',
|
'event_count' => $event['y'] ?? 0,
|
'created_at' => current_time('mysql')
|
]);
|
}
|
}
|
|
$this->logDebug('Successfully collected daily data', ['date' => $yesterday]);
|
}
|
|
/***************************************************************************
|
* HELPER METHODS
|
***************************************************************************/
|
|
/**
|
* Get current user type for tracking
|
*/
|
private function getCurrentUserType(): string
|
{
|
if (!is_user_logged_in()) {
|
return 'guest';
|
}
|
|
$user = wp_get_current_user();
|
$roles = $user->roles;
|
|
if (empty($roles)) {
|
return 'user';
|
}
|
|
$role = array_values($roles)[0];
|
return str_replace(BASE, '', $role);
|
}
|
|
/**
|
* Determine traffic source
|
*/
|
private function getTrafficSource(): string
|
{
|
if (isset($_GET['utm_source'])) {
|
return sanitize_text_field($_GET['utm_source']);
|
}
|
|
if (isset($_SERVER['HTTP_REFERER'])) {
|
$referer = $_SERVER['HTTP_REFERER'];
|
$site_url = get_site_url();
|
|
if (strpos($referer, $site_url) === false) {
|
$domain = parse_url($referer, PHP_URL_HOST);
|
return $domain ?: 'external';
|
}
|
|
return 'internal';
|
}
|
|
return 'direct';
|
}
|
|
/**
|
* Get website ID
|
*/
|
public function getWebsiteId(): string
|
{
|
$this->ensureInitialized();
|
return $this->website_id;
|
}
|
|
/**
|
* Get tracking script URL
|
*/
|
public function getTrackingScriptUrl(): string
|
{
|
if (!empty($this->api_url)) {
|
return rtrim($this->api_url, '/') . '/script.js';
|
}
|
|
// Default to Umami cloud
|
return 'https://cloud.umami.is/script.js';
|
}
|
|
/***************************************************************************
|
* REQUIRED ABSTRACT METHOD IMPLEMENTATIONS
|
***************************************************************************/
|
|
/**
|
* Get request headers for API calls
|
*/
|
protected function getRequestHeaders(): array
|
{
|
$headers = [
|
'Accept' => 'application/json',
|
'Content-Type' => 'application/json'
|
];
|
|
if (!empty($this->api_token)) {
|
$headers['Authorization'] = 'Bearer ' . $this->api_token;
|
}
|
|
return $headers;
|
}
|
|
protected function handleRefreshData():array
|
{
|
// Collect today's data
|
$today = date('Y-m-d');
|
$data = $this->getAnalytics($today, $today, false);
|
|
if ($data) {
|
// Clear cache for today
|
$cache_key = md5("analytics_{$today}_{$today}");
|
$this->cache->forget($cache_key);
|
|
return [
|
'success' => true,
|
'message' => 'Data refreshed successfully',
|
'data' => $data
|
];
|
}
|
|
return [
|
'success' => false,
|
'message' => 'Something went wrong...'
|
];
|
}
|
|
/**
|
* Process queued operations
|
*/
|
public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
|
{
|
$prefix = strtolower($this->service_name) . '_';
|
|
switch ($operation->type) {
|
case $prefix . 'collect_analytics':
|
$this->collectDailyData();
|
return ['success' => true, 'message' => 'Analytics collected'];
|
|
default:
|
return $result;
|
}
|
}
|
|
/**
|
* Test API connection
|
*/
|
protected function performConnectionTest(): bool
|
{
|
// Basic tracking only needs website ID
|
if (empty($this->website_id)) {
|
return false;
|
}
|
|
// If API credentials provided, test them
|
if (!empty($this->api_url) && !empty($this->api_token)) {
|
try {
|
$response = wp_remote_get(rtrim($this->api_url, '/') . '/api/websites', [
|
'headers' => [
|
'Authorization' => 'Bearer ' . $this->api_token,
|
'Accept' => 'application/json'
|
],
|
'timeout' => 10
|
]);
|
|
if (is_wp_error($response)) {
|
return false;
|
}
|
|
return wp_remote_retrieve_response_code($response) === 200;
|
|
} catch (Exception $e) {
|
$this->logError('Connection test failed', ['error' => $e->getMessage()]);
|
return false;
|
}
|
}
|
|
// Website ID exists, basic tracking will work
|
return true;
|
}
|
|
/**
|
* Get service description
|
*/
|
public function getServiceDescription(): string
|
{
|
return "Privacy-focused analytics to understand your website traffic.";
|
}
|
|
/**
|
* Create database tables for metrics storage
|
*/
|
public static function createTables(): void
|
{
|
global $wpdb;
|
$charset_collate = $wpdb->get_charset_collate();
|
|
// Events table
|
$events_table = $wpdb->prefix . BASE . 'umami_events';
|
$events_sql = "CREATE TABLE IF NOT EXISTS $events_table (
|
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
date date NOT NULL,
|
event_name varchar(255) NOT NULL,
|
event_count int(11) NOT NULL DEFAULT 0,
|
event_data longtext,
|
created_at datetime NOT NULL,
|
PRIMARY KEY (id),
|
KEY date_event (date, event_name)
|
) $charset_collate;";
|
|
// Metrics table
|
$metrics_table = $wpdb->prefix . BASE . 'performance_metrics';
|
$metrics_sql = "CREATE TABLE IF NOT EXISTS $metrics_table (
|
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
date date NOT NULL,
|
pageviews int(11) NOT NULL DEFAULT 0,
|
visitors int(11) NOT NULL DEFAULT 0,
|
visits int(11) NOT NULL DEFAULT 0,
|
bounces int(11) NOT NULL DEFAULT 0,
|
total_time int(11) NOT NULL DEFAULT 0,
|
created_at datetime NOT NULL,
|
PRIMARY KEY (id),
|
UNIQUE KEY date_unique (date)
|
) $charset_collate;";
|
|
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
dbDelta($events_sql);
|
dbDelta($metrics_sql);
|
}
|
}
|