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 { if (!$this->isSetUp() || is_admin()) { return; } // Skip on local environments if (strpos(get_home_url(), JVB_LOCAL) !== false) { 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->delete($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); } }