当前位置: 首页 > news >正文

自建Umami访问统计服务并通过分享链接进行博客公开统计

前言

我想展示umami数据,但是自托管的貌似没有api,经过探索发现可以通过分享链接拿到数据

我的blogblog.dorimu.cn-umami-share-stats

抓包分析

发现分析界面 https://charity.dorimu.cn/share/xxx 获取数据分两步:

  1. GET /api/share/{shareId}
  2. GET /api/websites/{websiteId}/stats?...,请求头带 x-umami-share-token

第一步返回 websiteId + token,第二步返回统计数据(pageviewsvisitorsvisits 等)。

示例

GET https://charity.dorimu.cn/api/share/abc123

响应(示例):

{"websiteId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx","token": "eyJhbGciOi..."
}

站点统计:

GET https://charity.dorimu.cn/api/websites/{websiteId}/stats?startAt=0&endAt=1730000000000
x-umami-share-token: {token}

拿单页面统计时,加 path 参数:

GET https://charity.dorimu.cn/api/websites/{websiteId}/stats?startAt=0&endAt=1730000000000&path=%2Fposts%2Fhello-world%2F
x-umami-share-token: {token}

注意:path 要 URL 编码,而且路径要和你实际上报的路径完全一致(尤其是尾斜杠)。

umami-share.js 完整代码

我是更改 Astro & Mizuki 里面的umami-share.js

((global) => {const CACHE_PREFIX = "umami-share-cache";const STATS_CACHE_TTL = 3600_000; // 1hconst SHARE_INFO_CACHE_TTL = 3600_000; // 10minfunction normalizeBaseUrl(baseUrl = "") {return String(baseUrl).trim().replace(/\/+$/, "");}function normalizeApiBase(baseUrl = "") {const normalized = normalizeBaseUrl(baseUrl);if (!normalized) return "";return normalized.endsWith("/api") ? normalized : `${normalized}/api`;}function normalizeV1Base(baseUrl = "") {const normalized = normalizeBaseUrl(baseUrl);if (!normalized) return "";return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;}function getStorageItem(key) {try {return localStorage.getItem(key);} catch {return null;}}function setStorageItem(key, value) {try {localStorage.setItem(key, value);} catch {// 忽略 localStorage 不可用场景}}function removeStorageItem(key) {try {localStorage.removeItem(key);} catch {// 忽略 localStorage 不可用场景}}function createCacheKey(parts) {return `${CACHE_PREFIX}:${parts.join(":")}`;}function readCache(key, ttl) {const raw = getStorageItem(key);if (!raw) return null;try {const parsed = JSON.parse(raw);if (Date.now() - parsed.timestamp < ttl) {return parsed.value;}removeStorageItem(key);} catch {removeStorageItem(key);}return null;}function writeCache(key, value) {setStorageItem(key,JSON.stringify({timestamp: Date.now(),value,}),);}function parseShareIdFromShareUrl(shareUrl = "") {if (!shareUrl) return "";try {const url = new URL(shareUrl);const match = url.pathname.match(/\/share\/([^/?#]+)/);return match?.[1] || "";} catch {return "";}}function parseBaseUrlFromUrl(value = "") {if (!value) return "";try {return normalizeBaseUrl(new URL(value).origin);} catch {return "";}}function parseBaseUrlFromScripts(scripts = "") {if (typeof scripts === "string" && scripts) {const scriptSrc = scripts.match(/src="([^"]+)"/)?.[1] || "";const parsed = parseBaseUrlFromUrl(scriptSrc);if (parsed) return parsed;}const runtimeScript = document.querySelector('script[data-website-id][src*="script.js"]',);if (runtimeScript instanceof HTMLScriptElement && runtimeScript.src) {return parseBaseUrlFromUrl(runtimeScript.src);}return "";}function normalizeTimestamp(value, defaultValue) {const numeric = Number(value);return Number.isFinite(numeric) ? numeric : defaultValue;}function buildStatsUrl(baseUrl, websiteId, urlPath, startAt, endAt) {const apiBase = normalizeApiBase(baseUrl);if (!apiBase) {throw new Error("缺少 Umami baseUrl");}const params = new URLSearchParams({startAt: String(startAt),endAt: String(endAt),});if (urlPath) {params.set("path", urlPath);}return `${apiBase}/websites/${encodeURIComponent(websiteId)}/stats?${params.toString()}`;}async function fetchJson(url, headers = {}) {const response = await fetch(url, { headers });if (!response.ok) {throw new Error(`${response.status} ${response.statusText}`);}return response.json();}async function fetchShareInfo(baseUrl, shareId) {if (!shareId) {throw new Error("缺少 Umami shareId");}const normalizedBase = normalizeBaseUrl(baseUrl);if (!normalizedBase) {throw new Error("缺少 Umami baseUrl");}const cacheKey = createCacheKey(["share-info",encodeURIComponent(normalizedBase),shareId,]);const cached = readCache(cacheKey, SHARE_INFO_CACHE_TTL);if (cached?.token && cached?.websiteId) {return cached;}const apiBase = normalizeApiBase(normalizedBase);const shareInfo = await fetchJson(`${apiBase}/share/${encodeURIComponent(shareId)}`,);if (!shareInfo?.token || !shareInfo?.websiteId) {throw new Error("Umami 分享接口返回数据不完整");}writeCache(cacheKey, shareInfo);return shareInfo;}function normalizeInputOptions(baseUrlOrOptions, apiKey, websiteId) {const defaults = {baseUrl: "",apiKey: "",websiteId: "",shareId: "",shareUrl: "",scripts: "",urlPath: "",startAt: undefined,endAt: undefined,autoRange: false,};let options = defaults;if (baseUrlOrOptions &&typeof baseUrlOrOptions === "object" &&!Array.isArray(baseUrlOrOptions)) {options = {...defaults,...baseUrlOrOptions,};} else {options = {...defaults,baseUrl: baseUrlOrOptions || "",apiKey: apiKey || "",websiteId: websiteId || "",};}options.baseUrl = normalizeBaseUrl(options.baseUrl || "");options.apiKey = String(options.apiKey || "").trim();options.websiteId = String(options.websiteId || "").trim();options.shareId = String(options.shareId || "").trim();options.shareUrl = String(options.shareUrl || "").trim();options.scripts = String(options.scripts || "");options.urlPath = String(options.urlPath || "");const hasStartAt =options.startAt !== undefined && options.startAt !== null && options.startAt !== "";const hasEndAt =options.endAt !== undefined && options.endAt !== null && options.endAt !== "";options.startAt = hasStartAt ? normalizeTimestamp(options.startAt, 0) : 0;options.endAt = hasEndAt? normalizeTimestamp(options.endAt, Date.now()): Date.now();options.autoRange = !hasStartAt && !hasEndAt;if (!options.shareId && options.shareUrl) {options.shareId = parseShareIdFromShareUrl(options.shareUrl);}if (!options.baseUrl) {if (options.shareUrl) {options.baseUrl = parseBaseUrlFromUrl(options.shareUrl);}if (!options.baseUrl) {options.baseUrl = parseBaseUrlFromScripts(options.scripts);}}return options;}function buildStatsCacheKey(mode, options) {return createCacheKey(["stats",mode,encodeURIComponent(options.baseUrl || ""),options.websiteId || "__unknown__",options.shareId || "__none__",encodeURIComponent(options.urlPath || "__site__"),String(options.startAt),options.autoRange ? "__auto__" : String(options.endAt),]);}async function fetchStatsWithShare(options) {const shareInfo = await fetchShareInfo(options.baseUrl, options.shareId);const websiteId = options.websiteId || shareInfo.websiteId;if (!websiteId) {throw new Error("分享接口未返回 websiteId");}const statsUrl = buildStatsUrl(options.baseUrl,websiteId,options.urlPath,options.startAt,options.endAt,);return fetchJson(statsUrl, {"x-umami-share-token": shareInfo.token,});}async function fetchStatsWithApiKey(options) {if (!options.baseUrl) {throw new Error("缺少 Umami baseUrl");}if (!options.apiKey) {throw new Error("缺少 Umami apiKey");}if (!options.websiteId) {throw new Error("缺少 Umami websiteId");}const v1Base = normalizeV1Base(options.baseUrl);const params = new URLSearchParams({startAt: String(options.startAt),endAt: String(options.endAt),});if (options.urlPath) {params.set("path", options.urlPath);}const statsUrl = `${v1Base}/websites/${encodeURIComponent(options.websiteId)}/stats?${params.toString()}`;return fetchJson(statsUrl, {"x-umami-api-key": options.apiKey,});}async function fetchStats(baseUrlOrOptions, apiKey, websiteId) {const options = normalizeInputOptions(baseUrlOrOptions, apiKey, websiteId);const mode = options.shareId ? "share" : options.apiKey ? "api-key" : "";if (!mode) {throw new Error("缺少 Umami 认证信息,请配置 shareId/shareUrl(推荐)或 apiKey",);}const cacheKey = buildStatsCacheKey(mode, options);const cached = readCache(cacheKey, STATS_CACHE_TTL);if (cached) {return cached;}const stats =mode === "share"? await fetchStatsWithShare(options): await fetchStatsWithApiKey(options);writeCache(cacheKey, stats);return stats;}global.getUmamiWebsiteStats = async (baseUrlOrOptions, apiKey, websiteId) => {try {return await fetchStats(baseUrlOrOptions, apiKey, websiteId);} catch (err) {throw new Error(`获取Umami统计数据失败: ${err.message}`);}};global.getUmamiPageStats = async (baseUrlOrOptions,apiKey,websiteId,urlPath,startAt,endAt,) => {try {let options = baseUrlOrOptions;if (baseUrlOrOptions &&typeof baseUrlOrOptions === "object" &&!Array.isArray(baseUrlOrOptions)) {options = {...baseUrlOrOptions,};if (typeof urlPath === "string") {options.urlPath = urlPath;}if (startAt !== undefined) {options.startAt = startAt;}if (endAt !== undefined) {options.endAt = endAt;}} else {options = {baseUrl: baseUrlOrOptions,apiKey,websiteId,urlPath,startAt,endAt,};}return await fetchStats(options);} catch (err) {throw new Error(`获取Umami页面统计数据失败: ${err.message}`);}};global.clearUmamiShareCache = () => {try {for (let index = localStorage.length - 1; index >= 0; index -= 1) {const key = localStorage.key(index);if (key && key.startsWith(`${CACHE_PREFIX}:`)) {localStorage.removeItem(key);}}} catch {// 忽略 localStorage 不可用场景}};
})(window);

配置

环境变量推荐:

UMAMI_SHARE_ID=abc123
http://www.jsqmd.com/news/387252/

相关文章:

  • ethercat
  • 写论文省心了 8个一键生成论文工具测评:专科生毕业论文+科研写作全攻略
  • 2026冲刺用!8个降AIGC平台测评:本科生降AI率必备工具推荐
  • 掌握低查重AI教材写作,利用AI工具轻松生成优质教材
  • AlphaEarth Foundations:AI重塑地球测绘技术
  • 2026别错过!9个降AI率工具深度测评,研究生必看的降AIGC神器推荐
  • 从基础到企业级:为Gin RESTful API添加认证、配置与错误处理
  • 建议收藏|10个一键生成论文工具深度测评:MBA毕业论文+学术写作必备神器
  • A实验:小鼠糖水偏好实验专业的平台 大小鼠糖水偏爱实验系统 细节资料。
  • 照着用就行:10个一键生成论文工具测评!本科生毕业论文+开题报告高效写作指南
  • 机器视觉-硬件方案方案组成
  • 软件工程:小组开发过程技术(VS VSS UNIX C++)完整教程:从入门到实战部署
  • 琐事如尘,生活如花
  • 家和万事兴
  • 2026年采购参考:评测朝阳区多家施耐德电气生产实力,施耐德电气/电气自动化/中低压电气,施耐德电气直销厂家哪家好 - 品牌推荐师
  • 2026年国内靠谱的闸阀制造商电话,除尘阀门/阀门/烟气阀门/V型球阀/波纹管截止阀/喷煤球阀,闸阀实力厂家排名 - 品牌推荐师
  • Claude Opus 4.6 《癸酉本红楼梦》深度分析
  • 大厂踩刹车,小厂赌身家:AI眼镜为何迟迟等不到“iPhone时刻”?
  • Zenith.NET v0.0.6 发布 — API 大幅精简,为 Metal 后端铺路
  • 扫描深孔流道用什么探头模式?专业三维扫描解决方案汇总 - 匠言榜单
  • 2025-2026年GEO加盟代理服务商深度测评与推荐报告(摘星AI重点解析) - 2026年企业推荐榜
  • 2026年备考初二地生,同步练习册精选指南,期中自测卷/重点名校卷/冲刺卷/中考卷,同步练习册生产厂家口碑推荐 - 品牌推荐师
  • LAN9253中文注释第八章
  • vue3+nodejs微信小程序人脸识别的游泳馆会员管理系统
  • vue3微信小程序-基于nodejs的服装穿搭推荐系统
  • Vue3+nodejs的显卡之家 二手显卡商城交易系统 开题
  • vue3基于nodejs的智慧社区活动商品管理系统的设计与实现
  • 2026不锈钢热轧板直销,哪家厂家口碑更佳?不锈钢酸洗管/不锈钢楼梯扶手管/不锈钢精密管,不锈钢热轧板生产加工找哪家 - 品牌推荐师
  • 实用指南:APP广告变现数据分析:关键指标与优化策略
  • 别再折腾OpenClaw部署啦!Kimi推出KimiClaw,原生集成OpenClaw,问题来了,你有Kimi会员体验吗?