前言
我想展示umami数据,但是自托管的貌似没有api,经过探索发现可以通过分享链接拿到数据
我的blogblog.dorimu.cn-umami-share-stats
抓包分析
发现分析界面 https://charity.dorimu.cn/share/xxx 获取数据分两步:
- GET /api/share/{shareId}
- GET /api/websites/{websiteId}/stats?...,请求头带 x-umami-share-token
第一步返回 websiteId + token,第二步返回统计数据(pageviews、visitors、visits 等)。
示例
- 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; // 1h
- const SHARE_INFO_CACHE_TTL = 3600_000; // 10min
- function 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);
复制代码 配置
环境变量推荐:来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |