2519 字
13 分钟
Umami统计系统集成与问题排查实战

Umami统计系统集成与问题排查实战#

Umami是一个开源、隐私友好的网站统计分析工具,相比Google Analytics更加轻量且注重用户隐私。本文将基于真实项目经验,详细介绍Umami的集成配置以及常见问题的排查与解决方案。

目录#

  1. Umami简介与优势
  2. 基础配置与集成
  3. 高级配置选项
  4. 常见问题诊断
  5. 性能优化策略
  6. 实战案例分析
  7. 最佳实践建议

Umami简介与优势#

为什么选择Umami?#

特性UmamiGoogle Analytics其他统计工具
隐私保护✅ 完全匿名❌ 收集个人数据⚠️ 因工具而异
开源免费✅ 完全开源❌ 闭源免费⚠️ 部分开源
自主部署✅ 支持❌ 仅云服务⚠️ 部分支持
轻量级✅ <2KB❌ >45KB⚠️ 因工具而异
GDPR合规✅ 天然合规❌ 需额外配置⚠️ 因工具而异

核心功能特性#

  • 实时统计:页面浏览量、独立访客、会话数据
  • 地理分析:访客地理位置分布
  • 设备统计:浏览器、操作系统、设备类型
  • 事件追踪:自定义事件统计
  • API支持:完整的REST API

基础配置与集成#

1. Umami服务部署#

使用Umami Cloud(推荐)#

Terminal window
# 注册账号
https://cloud.umami.is
# 创建网站
Website ID: your-website-id
Domain: your-domain.com

自主部署#

Terminal window
# 使用Docker部署
git clone https://github.com/umami-software/umami.git
cd umami
docker-compose up -d

2. 前端脚本集成#

基础配置#

Layout.astro
---
import { siteConfig } from '../config';
---
<head>
<!-- Umami Analytics -->
{siteConfig.analytics?.umami?.websiteId && (
<script
async
defer
src={siteConfig.analytics.umami.src || 'https://umami.is/script.js'}
data-website-id={siteConfig.analytics.umami.websiteId}
data-domains={siteConfig.analytics.umami.domains}
data-auto-track="true"
data-cache="true"
onload="console.log('[Umami] Script loaded successfully')"
onerror="console.error('[Umami] Failed to load script')"
></script>
)}
</head>

配置文件设置#

config.ts
export const siteConfig = {
analytics: {
umami: {
websiteId: 'your-website-id',
src: 'https://umami.is/script.js',
domains: 'blog.your-domain.com'
}
}
};

3. 环境变量配置#

.env
UMAMI_WEBSITE_ID=080b7a8a-bc05-4651-9def-9006601aae3d
UMAMI_API_TOKEN=your-api-token
UMAMI_API_URL=https://cloud.umami.is

高级配置选项#

1. 自动追踪配置#

<!-- 启用自动追踪 -->
<script
data-auto-track="true"
data-cache="true"
data-domains="your-domain.com"
data-host-url="https://your-umami-instance.com"
src="https://umami.is/script.js"
></script>

2. 手动事件追踪#

// 等待Umami加载
function waitForUmami() {
if (window.umami && typeof window.umami.track === 'function') {
window.umamiReady = true;
console.log('[Umami] Ready for tracking');
return true;
}
setTimeout(waitForUmami, 500);
return false;
}
// 手动追踪事件
function trackCustomEvent(eventName, eventData = {}) {
if (window.umamiReady && window.umami) {
window.umami.track(eventName, eventData);
console.log(`[Umami] Event tracked: ${eventName}`, eventData);
} else {
console.warn('[Umami] Not ready, event not tracked:', eventName);
}
}
// 使用示例
trackCustomEvent('button-click', { button: 'download' });
trackCustomEvent('form-submit', { form: 'contact' });

3. 页面视图追踪#

// 手动页面追踪(适用于SPA)
function trackPageView(url, title) {
if (window.umami) {
window.umami.track(props => ({
...props,
url: url,
title: title
}));
}
}
// 路由变化时调用
trackPageView('/new-page', 'New Page Title');

常见问题诊断#

1. 脚本加载失败#

问题症状#

// 控制台错误
[ERROR] Failed to load resource: net::ERR_BLOCKED_BY_CLIENT
[WARNING] [Umami] Umami not available yet, retrying in 500ms

诊断步骤#

// 1. 检查脚本加载状态
console.log('Umami script element:', document.querySelector('[data-website-id]'));
console.log('Umami object:', window.umami);
// 2. 检查网络请求
fetch('https://umami.is/script.js')
.then(response => console.log('Script accessible:', response.ok))
.catch(error => console.error('Script blocked:', error));
// 3. 检查域名配置
const scriptElement = document.querySelector('[data-website-id]');
console.log('Configured domains:', scriptElement?.getAttribute('data-domains'));
console.log('Current hostname:', window.location.hostname);

解决方案#

// 方案1: 更新脚本源地址
const UMAMI_SOURCES = [
'https://umami.is/script.js',
'https://cloud.umami.is/script.js',
'https://analytics.umami.is/script.js'
];
async function loadUmamiScript() {
for (const src of UMAMI_SOURCES) {
try {
const response = await fetch(src);
if (response.ok) {
// 动态加载脚本
const script = document.createElement('script');
script.src = src;
script.setAttribute('data-website-id', 'your-website-id');
script.setAttribute('data-domains', 'your-domain.com');
document.head.appendChild(script);
break;
}
} catch (error) {
console.warn(`Failed to load from ${src}:`, error);
}
}
}

2. 域名配置错误#

问题症状#

// Umami脚本加载但不记录数据
[LOG] [Umami] Script loaded successfully
[WARNING] Domain mismatch: current domain not in allowed list

解决方案#

// 动态域名配置
function configureDomains() {
const currentDomain = window.location.hostname;
const allowedDomains = [
'localhost',
'your-domain.com',
'www.your-domain.com',
'blog.your-domain.com'
];
if (!allowedDomains.includes(currentDomain)) {
console.warn(`[Umami] Current domain ${currentDomain} not in allowed list`);
return false;
}
// 更新脚本配置
const scriptElement = document.querySelector('[data-website-id]');
if (scriptElement) {
scriptElement.setAttribute('data-domains', allowedDomains.join(','));
}
return true;
}

3. 重复初始化问题#

问题症状#

// 变量重复声明错误
SyntaxError: Failed to execute 'replaceWith' on 'Element':
Identifier 'retryCount' has already been declared

解决方案#

// 防止重复初始化
(function() {
// 检查是否已初始化
if (window.umamiInitialized) {
return;
}
window.umamiInitialized = true;
// 全局状态管理
window.umamiTracking = {
initialized: false,
ready: false,
retryCount: 0,
maxRetries: 20
};
function initializeUmami() {
const state = window.umamiTracking;
if (window.umami && typeof window.umami.track === 'function') {
state.ready = true;
state.initialized = true;
console.log('[Umami] Ready for tracking');
return true;
}
if (state.retryCount < state.maxRetries) {
state.retryCount++;
setTimeout(initializeUmami, 500);
} else {
console.warn('[Umami] Failed to initialize after', state.maxRetries, 'attempts');
}
return false;
}
// 文档就绪后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeUmami);
} else {
initializeUmami();
}
})();

4. API数据获取问题#

问题症状#

// API返回空数据
{pageviews: 0, visitors: 0}

诊断代码#

// API调试工具
async function debugUmamiAPI(url) {
const debugInfo = {
requestUrl: url,
timestamp: new Date().toISOString(),
decodedUrl: decodeURIComponent(url)
};
try {
const response = await fetch(`/api/stats?url=${encodeURIComponent(url)}&debug=true`);
const data = await response.json();
debugInfo.response = data;
debugInfo.status = response.status;
console.log('[Umami API Debug]', debugInfo);
return data;
} catch (error) {
debugInfo.error = error.message;
console.error('[Umami API Debug]', debugInfo);
throw error;
}
}
// 使用示例
debugUmamiAPI('/posts/your-article-slug/');

URL编码问题修复#

// 统一URL格式处理
function normalizeUrl(url) {
try {
// 解码URL
const decoded = decodeURIComponent(url);
// 规范化路径
const normalized = decoded
.replace(/\/+/g, '/') // 移除重复斜杠
.replace(/\/$/, '') || '/'; // 移除尾部斜杠,根路径除外
console.log(`[URL Normalize] ${url} -> ${normalized}`);
return normalized;
} catch (error) {
console.warn('[URL Normalize] Failed:', error);
return url;
}
}
// 在API调用中使用
async function fetchStats(rawUrl) {
const normalizedUrl = normalizeUrl(rawUrl);
const endpoint = `/api/stats?url=${encodeURIComponent(normalizedUrl)}`;
return fetch(endpoint);
}

性能优化策略#

1. 延迟加载#

// 延迟加载统计脚本
function loadUmamiWhenIdle() {
if ('requestIdleCallback' in window) {
requestIdleCallback(loadUmami);
} else {
setTimeout(loadUmami, 100);
}
}
function loadUmami() {
const script = document.createElement('script');
script.src = 'https://umami.is/script.js';
script.setAttribute('data-website-id', 'your-website-id');
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
// 页面加载完成后延迟加载
window.addEventListener('load', loadUmamiWhenIdle);

2. 缓存策略#

// API响应缓存
const CACHE_DURATION = 5 * 60 * 1000; // 5分钟
const statsCache = new Map();
async function getCachedStats(url) {
const cacheKey = url;
const cached = statsCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
console.log('[Stats Cache] Hit:', cacheKey);
return cached.data;
}
try {
const data = await fetchStats(url);
statsCache.set(cacheKey, {
data,
timestamp: Date.now()
});
console.log('[Stats Cache] Miss, cached:', cacheKey);
return data;
} catch (error) {
// 返回缓存数据(如果有)
if (cached) {
console.log('[Stats Cache] Error, using stale cache:', cacheKey);
return cached.data;
}
throw error;
}
}

3. 批量请求优化#

// 批量获取统计数据
class StatsManager {
constructor() {
this.queue = [];
this.processing = false;
this.batchSize = 5;
this.batchDelay = 100;
}
async getStats(url, element) {
return new Promise((resolve, reject) => {
this.queue.push({ url, element, resolve, reject });
this.processBatch();
});
}
async processBatch() {
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
while (this.queue.length > 0) {
const batch = this.queue.splice(0, this.batchSize);
try {
await Promise.all(
batch.map(async ({ url, element, resolve, reject }) => {
try {
const data = await getCachedStats(url);
this.updateElement(element, data);
resolve(data);
} catch (error) {
reject(error);
}
})
);
} catch (error) {
console.error('[Stats Batch] Error:', error);
}
// 批次间延迟
if (this.queue.length > 0) {
await new Promise(resolve => setTimeout(resolve, this.batchDelay));
}
}
this.processing = false;
}
updateElement(element, data) {
if (!element) return;
const { pageviews, visitors } = data;
const viewsEl = element.querySelector('[data-stats="views"]');
const visitorsEl = element.querySelector('[data-stats="visitors"]');
if (viewsEl) {
viewsEl.textContent = pageviews > 0 ? `${pageviews} 次浏览` : '暂无数据';
}
if (visitorsEl) {
visitorsEl.textContent = visitors > 0 ? `${visitors} 位访客` : '暂无数据';
}
}
}
// 使用示例
const statsManager = new StatsManager();
// 为多个文章卡片获取统计数据
document.querySelectorAll('[data-post-url]').forEach(element => {
const url = element.getAttribute('data-post-url');
statsManager.getStats(url, element);
});

实战案例分析#

案例:博客统计数据不一致#

问题描述#

  • 主页文章卡片显示:213 次浏览,18 位访客
  • 文章内页显示:暂无数据

问题分析#

// 1. URL格式对比
console.log('PostCard URL:', '/posts/文章标题/');
console.log('PostMeta URL:', '/posts/%E6%96%87%E7%AB%A0%E6%A0%87%E9%A2%98/');
console.log('URL编码不匹配!');
// 2. API请求对比
console.log('PostCard API:', '/api/stats?url=%2Fposts%2F文章标题%2F');
console.log('PostMeta API:', '/api/stats?url=%2Fposts%2F%25E6%2596%2587%25E7%25AB%25A0%25E6%25A0%2587%25E9%25A2%2598%2F');
console.log('双重编码问题!');

解决方案#

// 修复前:文章内页统计
const currentPath = window.location.pathname; // 已编码
const endpoint = `/api/stats?url=${encodeURIComponent(currentPath)}`; // 双重编码
// 修复后:统一URL格式
const currentPath = decodeURIComponent(window.location.pathname); // 解码
const endpoint = `/api/stats?url=${encodeURIComponent(currentPath)}`; // 单次编码
console.log('[Stats Fix] URL normalized:', currentPath);

验证结果#

// 修复后的统计数据
console.log('[Stats] PostCard:', { pageviews: 213, visitors: 18 });
console.log('[Stats] PostMeta:', { pageviews: 213, visitors: 18 });
console.log('[Stats] Data consistency: ✅');

最佳实践建议#

1. 配置管理#

// 统一配置管理
interface UmamiConfig {
websiteId: string;
apiUrl: string;
apiToken: string;
domains: string[];
autoTrack: boolean;
cache: boolean;
}
const umamiConfig: UmamiConfig = {
websiteId: process.env.UMAMI_WEBSITE_ID!,
apiUrl: process.env.UMAMI_API_URL!,
apiToken: process.env.UMAMI_API_TOKEN!,
domains: ['localhost', 'your-domain.com'],
autoTrack: true,
cache: true
};

2. 错误监控#

// Umami错误监控
class UmamiMonitor {
constructor() {
this.errors = [];
this.setupErrorHandling();
}
setupErrorHandling() {
// 监听脚本加载错误
document.addEventListener('error', (event) => {
if (event.target.src && event.target.src.includes('umami')) {
this.logError('Script Load Error', {
src: event.target.src,
message: 'Failed to load Umami script'
});
}
}, true);
// 监听API错误
const originalFetch = window.fetch;
window.fetch = async (...args) => {
try {
const response = await originalFetch(...args);
if (args[0].includes('/api/stats') && !response.ok) {
this.logError('API Error', {
url: args[0],
status: response.status,
statusText: response.statusText
});
}
return response;
} catch (error) {
if (args[0].includes('/api/stats')) {
this.logError('Network Error', {
url: args[0],
error: error.message
});
}
throw error;
}
};
}
logError(type, details) {
const error = {
type,
details,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
};
this.errors.push(error);
console.error('[Umami Monitor]', error);
// 可选:发送错误报告
this.reportError(error);
}
async reportError(error) {
try {
await fetch('/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(error)
});
} catch (e) {
console.warn('[Umami Monitor] Failed to report error:', e);
}
}
getErrorSummary() {
return {
totalErrors: this.errors.length,
errorTypes: [...new Set(this.errors.map(e => e.type))],
recentErrors: this.errors.slice(-5)
};
}
}
// 启用监控
const monitor = new UmamiMonitor();

3. 测试策略#

// Umami功能测试
class UmamiTester {
async runTests() {
const results = [];
// 测试脚本加载
results.push(await this.testScriptLoading());
// 测试API连接
results.push(await this.testAPIConnection());
// 测试事件追踪
results.push(await this.testEventTracking());
// 测试URL处理
results.push(await this.testUrlHandling());
return results;
}
async testScriptLoading() {
return {
name: 'Script Loading',
passed: !!(window.umami && typeof window.umami.track === 'function'),
details: {
umamiExists: !!window.umami,
trackFunction: typeof window.umami?.track,
scriptElement: !!document.querySelector('[data-website-id]')
}
};
}
async testAPIConnection() {
try {
const response = await fetch('/api/stats?url=%2F');
const data = await response.json();
return {
name: 'API Connection',
passed: response.ok && typeof data.pageviews === 'number',
details: {
status: response.status,
hasPageviews: typeof data.pageviews === 'number',
hasVisitors: typeof data.visitors === 'number'
}
};
} catch (error) {
return {
name: 'API Connection',
passed: false,
details: { error: error.message }
};
}
}
async testEventTracking() {
if (!window.umami) {
return {
name: 'Event Tracking',
passed: false,
details: { error: 'Umami not loaded' }
};
}
try {
// 发送测试事件
window.umami.track('test-event', { test: true });
return {
name: 'Event Tracking',
passed: true,
details: { eventSent: true }
};
} catch (error) {
return {
name: 'Event Tracking',
passed: false,
details: { error: error.message }
};
}
}
async testUrlHandling() {
const testUrls = [
'/posts/测试文章/',
'/posts/%E6%B5%8B%E8%AF%95%E6%96%87%E7%AB%A0/',
'/posts/test-article/'
];
const results = {};
for (const url of testUrls) {
const normalized = decodeURIComponent(url);
const encoded = encodeURIComponent(normalized);
results[url] = {
original: url,
normalized: normalized,
encoded: encoded
};
}
return {
name: 'URL Handling',
passed: true,
details: results
};
}
}
// 运行测试
const tester = new UmamiTester();
tester.runTests().then(results => {
console.log('[Umami Tests]', results);
});

总结#

通过本文的详细介绍,我们掌握了Umami统计系统的:

  1. 完整集成流程:从部署到配置的全过程
  2. 问题诊断技巧:快速定位和解决常见问题
  3. 性能优化策略:提升统计功能的用户体验
  4. 最佳实践方案:确保系统稳定可靠运行

Umami作为一个隐私友好的统计工具,在正确配置和优化后能够提供准确、实时的网站分析数据。希望这份实战指南能帮助你成功集成Umami统计系统!

相关资源#

赞赏支持

如果这篇文章对你有帮助,欢迎请我喝杯咖啡 ☕

感谢您的支持,这将激励我创作更多优质内容!

Umami统计系统集成与问题排查实战
https://blog.muhan.wiki/posts/umami统计系统集成与问题排查实战/
作者
MuHan
发布于
2025-09-26
许可协议
CC BY-NC-SA 4.0