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 umami.is 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 '' . "\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 '' . "\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 '
attributesToString($attributes) . ' style="display:none">
' . "\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 'attributesToString($attributes) . ' style="display:none">
' . "\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);
}
}