总结摘要
Umami 是一个开源网页访问统计服务,类似Google Analytics。专注于简单,快速,隐私安全。
前言
官网如是说,Umami is a modern, privacy-focused analytics platform. An open-source alternative to Google Analytics, Mixpanel and Amplitude. 简单说,Umami 是一个开源网页访问统计服务,类似Google Analytics。专注于简单,快速,隐私安全。在使用 Hugo NexT (见Hugo NexT 主题使用记录 )过程当中,发现使用的网站统计插件是
51LA
,结果不是太满意,一是因为用的国外VPS,数据一直转圈圈,二是感觉数据掌握在别人手上,不可控,不放心,遂想自部署。虽有
plausible
可选择,简单比较了下,选用了
Umami
。
Umami Github:
https://github.com/umami-software/umami
Umami 文档:
https://umami.is/docs/
自部署
我采用 Docker 容器私有化部署。
新建 .env 文件
1
2
3
4
5
# .env
UMAMI_DB_USER=umami
UMAMI_DB_PASSWORD=umami
UMAMI_DB_NAME=umami
UMAMI_APP_SECRET=sSDoqeR6oA==
上述DB 涉及到的 USER、PASSWORD、NAME 可以自己设置。UMAMI_APP_SECRET 可以用命令 openssl rand -base64 16 生成 16 字节的 Base64 编码字符串。
新建 docker-compose.yml 文件
参考
https://github.com/umami-software/umami/blob/master/docker-compose.yml
生成 docker-compose.yml 文件,并修改如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
---
services :
umami :
image : ghcr . io / umami - software / umami : latest
networks :
- umami - network
ports :
- "127.0.0.1:3000:3000"
environment :
DATABASE_URL : postgresql : //$ { UMAMI_DB_USER }: $ { UMAMI_DB_PASSWORD } @ umami - db : 5432 /$ { UMAMI_DB_NAME }
APP_SECRET : $ { UMAMI_APP_SECRET }
TRACKER_SCRIPT_NAME : "mima.js"
COLLECT_API_ENDPOINT : "/mima"
depends_on :
umami - db :
condition : service_healthy
init : true
restart : always
healthcheck :
test : [ "CMD-SHELL" , "curl http://localhost:3000/api/heartbeat" ]
interval : 5 s
timeout : 5 s
retries : 5
umami - db :
image : postgres : 15 - alpine
networks :
- umami - network
#ports:
# - "15432:5432"
environment :
POSTGRES_DB : $ { UMAMI_DB_NAME }
POSTGRES_USER : $ { UMAMI_DB_USER }
POSTGRES_PASSWORD : $ { UMAMI_DB_PASSWORD }
volumes :
- ./ umami - db - data : / var / lib / postgresql / data
restart : always
healthcheck :
test : [ "CMD-SHELL" , "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}" ]
interval : 5 s
timeout : 5 s
retries : 5
networks :
umami - network :
driver : bridge
文件中:
- "127.0.0.1:53000:3000" 说明只能是本机才能访问,可以通过反代出来。TRACKER_SCRIPT_NAME: "mima.js" 是修改 JS 统计脚本文件名,避免被广告拦截插件拦。广告拦截插件会收集访问统计服务的跟踪脚本文件名称,并对疑似脚本的加载进行拦截。如果你的网站加载了一个名为 analytics.js 的脚本文件,那么极有可能会被拦截。可以通过设置 TRACKER_SCRIPT_NAME 环境变量,设置为你想要的名称。详见
Environment variables
。COLLECT_API_ENDPOINT: "/mima" 也是通过修改服务端数据收集接口以应对广告拦截插件拦截数据,默认值是 /api/send。详见
Environment variables
。部署使用
当部署成功以后,用浏览器打开
http://127.0.0.1:3000
即可进入 Umami 网站。
默认的登录用户名是admin,密码是umami。登录后记得修改,用户名和密码都可以修改。
至于里面的添加网站、在网站代码中集成 Umami 等操作请自行摸索完成。
这些都完成后,即可收集到被添加网站的浏览数据了。
数据统计显示
统计数据只显示在上述 umami 网站里没多大劲,总不能时时要登录才能显示。能否在其他网站如自己的博客网站实时显示统计数据呢?肯定可以,这就要用到 umami API 功能了。umami 提供了自部署版本和 cloud 版本,从
umami api 官方文档
可以看到,对于自部署的和 umami 的调用节点,以及 api key 的获取都是不一样。 umami cloud 可以在注册登录后,在管理后台看到 Api keys 的管理入口,进去之后直接创建 api key 就可以用了。自部署版本要费点功夫。
新建 View only 权限的用户
为安全起见,建议新建 View only 权限的用户用他的 API Token 获取 Umami 的统计数据。当然用上述的 admin 用户也是可以的,但有安全焦虑(虽然数据没啥用)。这是因为多就在大多博客是静态开源无服务器,所有代码都展示在前端,包括 API 调用。而 Umami 的 admin API 权限太大,如果使用 admin 权限的 API Token,那么这个 token 可以获取、修改、删除所有网站的数据,会有严重的安全隐患。
大至过程是:
用 admin 用户登录,新建一用户如 uview ,权限设为 View Only。 用 admin 用户登录,新建 Team 。进入 Team 里,把新建的 uview 用户加到 Team 里,并确认 Team 里 uview 用户的权限为 View Only。 用 admin 用户登录,进入 Team 里,添加需要添加统计的博客网站。注意:不是在 admin 用户里添加网站。 在博客网站代码里集成刚刚添加的 Umami ,待 Umami 收集,形成网站统计数据,以便 api 能调到数据。 获取 View only 用户的 API Token
如上述,自部署版本获取 API Token 要费点功夫。经本人尝试,至少有以下三种,看个人方便:
浏览器开发者工具查找
用浏览器打开自部署的 umami 网站,用上述生成的 View only 权限的用户登录。打开浏览器开发者工具(不同的浏览器不同的OS可能不一样,可能是Ctrl + Shift + I 或 F12 ),再刷新网页。
照上图所示,标4处 Bearer 后面的一大串就是该用户的 API Token 。
用 curl 获取
终端发送 curl 请求,username 使用View only 权限的用户名和密码:
1
2
3
curl -X POST https://umami_url/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"username", "password":"password"}'
响应体中我们能得到一个 token 。
用 api 工具获取
使用像 POSTMAN、hoppscotch 、PostWoman 等 api 工具发送请求获取,如可以使用
Hoppscotch
。 同样的方式,传送用户名、密码发送请求,能得到一个token
上述三种方式都可以获取到 API Token 。我发现同一个用户名,方式不一样,获取到的 API Token 有可能不一样,但都能用。官方虽有验证 Token 是否有效(见
Apiauthverify
),但目前好像应该没有有效期限的限制。
信息
自托管配置: 自己部署的Umami,其会话和Token的过期时间(如SESSION_EXPIRY_DAYS)可以通过环境变量(如.env文件)进行修改。
制作数据挂件嵌入到博客前端
官方文档的 API 接口资料相对比较少,可以查看学习
Website Statistics
。也可以借助上面提到的浏览器开发者工具,查看获取的数据要求及格式。
以下以我NexT 主题博客(Hugo NexT 主题使用记录 )为例,说明集成嵌入过程。
修改站点根目录下hugo.yaml文件
修改站点根目录下hugo.yaml文件里siteState.views.plugin: 部分,设为 umami (可以自己设,与下面统一一致即可)。
再修改hugo.yaml文件里analytics: 部分内容,全部注释掉。
新建修改siteinfo.html文件
复制 /themes/hugo-theme-next/layouts/_partials/sidebar/siteinfo.html 到 /layouts/_partials/sidebar/siteinfo.html 并修改,在 {{ if eq .Site.Params.siteState.views.plugin "51la" }} 配对结束 {{ end }} 的后面,添加如下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{{ if eq .Site.Params.siteState.views.plugin "umami" }}
< script src = "/js/3rd/umami/umamistats.js" ></ script >
< div class = "siteinfo-item" >
< div class = "item-name" >
< i class = "fa fa-user-plus" ></ i > {{ T "SiteInfoItems.todayViews" }}
</ div >
< div class = "item-count" id = "umTodayPv" >< i class = "fa fa-sync fa-spin" ></ i ></ div >
</ div >
< div class = "siteinfo-item" >
< div class = "item-name" >
< i class = "fa fa-user-clock" ></ i > {{ T "SiteInfoItems.yesterdayViews" }}
</ div >
< div class = "item-count" id = "umYesterdayPv" >< i class = "fa fa-sync fa-spin" ></ i ></ div >
</ div >
< div class = "siteinfo-item" >
< div class = "item-name" >
< i class = "fa fa-arrows-down-to-people" ></ i > {{ T "SiteInfoItems.monthViews" }}
</ div >
< div class = "item-count" id = "umMonthPv" >< i class = "fa fa-sync fa-spin" ></ i ></ div >
</ div >
< div class = "siteinfo-item" >
< div class = "item-name" >
< i class = "fa fa-users" ></ i > {{ T "SiteInfoItems.totalViews" }}
</ div >
< div class = "item-count" id = "umTotalPv" >< i class = "fa fa-sync fa-spin" ></ i ></ div >
</ div >
{{ end }}
上述代码中 SiteInfoItems.todayViews 等是在 /themes/hugo-theme-next/i18n/zh-cn.yaml 里定义的。
新建 umamistats.js 文件
新建 /static/js/3rd/umami/umamistats.js 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
document . addEventListener ( 'DOMContentLoaded' , () => {
umiTongji ();
});
//获取当前的北京时间
function getBeijingTime () {
return new Date ( new Date (). getTime () + ( parseInt ( new Date (). getTimezoneOffset () / 60 ) + 8 ) * 3600 * 1000 );
}
function umiTongji () {
var siteUrl = "https://aum.yiwan.org" ;
var siteApiStats = "/api/websites/" ;
var umiToken = "S8oFmShCalV8o+edsI45wP5" ; //获取到的liveony用户的 token
var umiId = "a521-244498b47a97" ; //获取到的 websiteId
var umiTime = Date . parse ( getBeijingTime ()); //现在的北京时间时间戳
var todayStart = getBeijingTime (). setHours ( 0 , 0 , 0 , 0 ); //现在今天零点的北京时间时间戳
var yesterdayStart = todayStart - 24 * 3600 * 1000 ; //现在昨天零点的北京时间时间戳
var monthStart = new Date ( getBeijingTime (). getFullYear (), getBeijingTime (). getMonth (), 1 ). getTime (); //现在北京时间点当月1日零点的时间戳
var umiUrl = siteUrl + siteApiStats + umiId + "/stats?startAt=" + todayStart + "&endAt=" + umiTime + "&unit=hour&timezone=Asia/Shanghai" ;
fetch ( umiUrl , {
method : 'GET' ,
mode : 'cors' ,
cache : 'default' ,
headers : {
'Authorization' : 'Bearer ' + umiToken ,
'Content-Type' : 'application/json'
}
})
. then ( res => res . json ())
. then ( resdata => {
document . querySelector ( '#umTodayPv' ). innerHTML = resdata [ "pageviews" ]; //今日访问量
});
var umiUrl = siteUrl + siteApiStats + umiId + "/stats?startAt=" + yesterdayStart + "&endAt=" + todayStart + "&unit=hour&timezone=Asia/Shanghai" ;
fetch ( umiUrl , {
method : 'GET' ,
mode : 'cors' ,
cache : 'default' ,
headers : {
'Authorization' : 'Bearer ' + umiToken ,
'Content-Type' : 'application/json'
}
})
. then ( res => res . json ())
. then ( resdata => {
document . querySelector ( '#umYesterdayPv' ). innerHTML = resdata [ "pageviews" ]; //昨日访问量
});
umiUrl = siteUrl + siteApiStats + umiId + "/stats?startAt=" + monthStart + "&endAt=" + umiTime + "&unit=day&timezone=Asia/Shanghai" ;
fetch ( umiUrl , {
method : 'GET' ,
mode : 'cors' ,
cache : 'default' ,
headers : {
'Authorization' : 'Bearer ' + umiToken ,
'Content-Type' : 'application/json'
}
})
. then ( res => res . json ())
. then ( resdata => {
document . querySelector ( '#umMonthPv' ). innerHTML = resdata [ "pageviews" ]; //本月访问量
});
umiUrl = siteUrl + siteApiStats + umiId + "/stats?startAt=0&endAt=" + umiTime + "&unit=day&timezone=Asia/Shanghai" ;
fetch ( umiUrl , {
method : 'GET' ,
mode : 'cors' ,
cache : 'default' ,
headers : {
'Authorization' : 'Bearer ' + umiToken ,
'Content-Type' : 'application/json'
}
})
. then ( res => res . json ())
. then ( resdata => {
document . querySelector ( '#umTotalPv' ). innerHTML = resdata [ "pageviews" ]; //总访问量
});
}
上面的 siteUrl 、siteApiStats、 umiToken、 umiId 等要适当修改成自己网站的信息。
注意
上述我的代码中取值用的是 resdata["pageviews"]才成功,有些参考文档里用的是 resdata.pageviews.value,反正我试了不行。
最后显示效果如下:
视自己情况其实还可以设置 visitors: "总访客数:" 和 pageViews: "页面浏览:",要适当修改上面 umamistats.js 文件,不细述。
上面 umamistats.js 文件还可以参考下面的进行编写,可能更加直观,易于理解点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 从配置文件中获取 umami 的配置
const website_id = 'a52247a97' ;
// 拼接请求地址
const request_url = 'https://aum.yiwan.org' + '/api/websites/' + website_id + '/stats' ;
const start_time = new Date ( '2024-01-01' ). getTime ();
const end_time = new Date (). getTime ();
const token = 'S88o+edsI45wP5' ;
// 检查配置是否为空
if ( ! website_id ) {
throw new Error ( "Umami website_id is empty" );
}
if ( ! request_url ) {
throw new Error ( "Umami request_url is empty" );
}
if ( ! start_time ) {
throw new Error ( "Umami start_time is empty" );
}
if ( ! token ) {
throw new Error ( "Umami token is empty" );
}
const params = new URLSearchParams ({
startAt : start_time ,
endAt : end_time ,
});
const request_header = {
method : "GET" ,
headers : {
"Content-Type" : "application/json" ,
"Authorization" : "Bearer " + token ,
},
};
async function allStats () {
try {
const response = await fetch ( ` ${ request_url } ? ${ params } ` , request_header );
const contentType = response . headers . get ( 'content-type' );
if ( ! contentType || ! contentType . includes ( 'application/json' )) {
const responseText = await response . text ();
throw new Error ( `服务器返回了非JSON响应 (状态码: ${ response . status } ): ${ responseText . substring ( 0 , 100 ) } ...` );
}
if ( ! response . ok ) {
const errorText = await response . text ();
throw new Error ( `HTTP error! status: ${ response . status } , message: ${ errorText } ` );
}
const data = await response . json ();
const uniqueVisitors = data [ "visitors" ]; //获取独立访客数
const pageViews = data [ "pageviews" ]; //.value; // 获取页面浏览量
let ele1 = document . querySelector ( "#umami-site-pv" )
if ( ele1 ) {
ele1 . textContent = pageViews ; // 设置页面浏览量
}
let ele2 = document . querySelector ( "#umami-site-uv" )
if ( ele2 ) {
ele2 . textContent = uniqueVisitors ;
}
console . log ( uniqueVisitors , pageViews );
console . log ( data );
} catch ( error ) {
console . error ( error );
return "-1" ;
}
}
allStats ();
参考致谢
在使用过程中,参考并借鉴了以下网站内容(可能没列全),表示感谢。否则,完全自己折腾,不知啥时能搞明白。