关于 Umami

Umami 是一个开源的、以隐私为中心的 Google Analytics 替代方案。Umami 提供强大的网络分析解决方案,不会侵犯用户隐私。此外,当自行托管 Umami 时,可以完全控制自己的数据。

查看 Umami 功能示例:🔗Live Demo

如何搭建

本文使用 Serverless 平台来搭建 Umami,应用部署在 Netlify,数据库使用 Supabase,其他平台参见 官网文档

小声哔哔

官网个纯英文的太坑了

数据库

Supabase 是一个兼容 PostgreSQL 的无服务器数据库平台。

其他数据库参见官网文档 Hosting Managed databases 部分。

  1. 登录 Supabase 并创建一个数据库,名字假如为 umami 。注意,创建时会让你输入Database Password,复制下来保存着,等下要用。Region指的是数据库地址,尽量选美国(US),因为离Netlify服务器比较近(猜的:D)

  2. 获取连接字符串(侧边栏setting->Database->滑倒下面的Connection Pooling)->复制Connection String

    并把复制的Database Password代替[YOUR-PASSWORD]

    注意:你需要在这段字符串后面增加一个 ?pgbouncer=true

    现在你的Connetion String应该类似于这样的

    postgres://postgres:[YOUR-PASSWORD]@db.eepnqjajbammmqbefgua.supabase.co:6543/postgres?pgbouncer=true
  3. 回到主页,我们初始化数据库。进入侧边栏SQL Editor->New Query,复制以下代码并运行

    初始化数据库
    CREATE SCHEMA IF NOT EXISTS "auth";
    CREATE SCHEMA IF NOT EXISTS "extensions";
    create extension if not exists "uuid-ossp" with schema extensions;
    create extension if not exists pgcrypto with schema extensions;
    create extension if not exists pgjwt with schema extensions;

    grant usage on schema public to postgres, anon, authenticated, service_role;
    grant usage on schema extensions to postgres, anon, authenticated, service_role;
    alter user supabase_admin SET search_path TO public, extensions; -- don't include the "auth" schema

    grant all privileges on all tables in schema public to postgres, anon, authenticated, service_role, supabase_admin;
    grant all privileges on all functions in schema public to postgres, anon, authenticated, service_role, supabase_admin;
    grant all privileges on all sequences in schema public to postgres, anon, authenticated, service_role, supabase_admin;

    alter default privileges in schema public grant all on tables to postgres, anon, authenticated, service_role;
    alter default privileges in schema public grant all on functions to postgres, anon, authenticated, service_role;
    alter default privileges in schema public grant all on sequences to postgres, anon, authenticated, service_role;

    alter default privileges for user supabase_admin in schema public grant all on sequences to postgres, anon, authenticated, service_role;
    alter default privileges for user supabase_admin in schema public grant all on tables to postgres, anon, authenticated, service_role;
    alter default privileges for user supabase_admin in schema public grant all on functions to postgres, anon, authenticated, service_role;

    alter role anon set statement_timeout = '3s';
    alter role authenticated set statement_timeout = '8s';

    -- 上面的代码作用是赋予数据库编辑权限,要不然之后部署的时候会报错(血泪的教训惹)

    -- CreateTable
    CREATE TABLE "account" (
    "user_id" SERIAL NOT NULL,
    "username" VARCHAR(255) NOT NULL,
    "password" VARCHAR(60) NOT NULL,
    "is_admin" BOOLEAN NOT NULL DEFAULT false,
    "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
    "updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY ("user_id")
    );

    -- CreateTable
    CREATE TABLE "event" (
    "event_id" SERIAL NOT NULL,
    "website_id" INTEGER NOT NULL,
    "session_id" INTEGER NOT NULL,
    "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
    "url" VARCHAR(500) NOT NULL,
    "event_type" VARCHAR(50) NOT NULL,
    "event_value" VARCHAR(50) NOT NULL,

    PRIMARY KEY ("event_id")
    );

    -- CreateTable
    CREATE TABLE "pageview" (
    "view_id" SERIAL NOT NULL,
    "website_id" INTEGER NOT NULL,
    "session_id" INTEGER NOT NULL,
    "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
    "url" VARCHAR(500) NOT NULL,
    "referrer" VARCHAR(500),

    PRIMARY KEY ("view_id")
    );

    -- CreateTable
    CREATE TABLE "session" (
    "session_id" SERIAL NOT NULL,
    "session_uuid" UUID NOT NULL,
    "website_id" INTEGER NOT NULL,
    "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
    "hostname" VARCHAR(100),
    "browser" VARCHAR(20),
    "os" VARCHAR(20),
    "device" VARCHAR(20),
    "screen" VARCHAR(11),
    "language" VARCHAR(35),
    "country" CHAR(2),

    PRIMARY KEY ("session_id")
    );

    -- CreateTable
    CREATE TABLE "website" (
    "website_id" SERIAL NOT NULL,
    "website_uuid" UUID NOT NULL,
    "user_id" INTEGER NOT NULL,
    "name" VARCHAR(100) NOT NULL,
    "domain" VARCHAR(500),
    "share_id" VARCHAR(64),
    "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY ("website_id")
    );

    -- CreateIndex
    CREATE UNIQUE INDEX "account.username_unique" ON "account"("username");

    -- CreateIndex
    CREATE INDEX "event_created_at_idx" ON "event"("created_at");

    -- CreateIndex
    CREATE INDEX "event_session_id_idx" ON "event"("session_id");

    -- CreateIndex
    CREATE INDEX "event_website_id_idx" ON "event"("website_id");

    -- CreateIndex
    CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at");

    -- CreateIndex
    CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id");

    -- CreateIndex
    CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at");

    -- CreateIndex
    CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id");

    -- CreateIndex
    CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at");

    -- CreateIndex
    CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid");

    -- CreateIndex
    CREATE INDEX "session_created_at_idx" ON "session"("created_at");

    -- CreateIndex
    CREATE INDEX "session_website_id_idx" ON "session"("website_id");

    -- CreateIndex
    CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid");

    -- CreateIndex
    CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id");

    -- CreateIndex
    CREATE INDEX "website_user_id_idx" ON "website"("user_id");

    -- AddForeignKey
    ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;

    -- AddForeignKey
    ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;

    -- AddForeignKey
    ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;

    -- AddForeignKey
    ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;

    -- AddForeignKey
    ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;

    -- AddForeignKey
    ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;

    -- CreateAdminUser
    INSERT INTO account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);

    Run后显示Success. No rows returned应该就成了。打开侧边栏Table editor应该可以看到里面多出来了5条数据

Netlify

此时数据库已经初始化完毕,接下来将应用部署在 Netlify 上。

  • 点击一键部署

  • 或者手动部署

    • Fork Umami 的仓库: https://github.com/umami-software/umami 到自己的 GitHub

    • 注册一个 Netlify 帐户并登录(已有跳过)。

    • 从 GitHub 上导入项目。

    • 添加所需的环境变量 DATABASE_URLHASH_SALT(随机字符串)以及 TRACKER_SCRIPT_NAMEDATABASE_URL 就是你刚刚编辑过的数据库链接,HASH_SALT 随便填,TRACKER_SCRIPT_NAME 是指插件脚本名称,由于默认的umami.js容易被浏览器插件屏蔽,故在此自定义脚本名称)

    • 避免被插件拦截

      使用外挂 JS 的方式来统计数据,虽然可以统计真实的访客记录,但是会被 uBlock 此类的广告拦截插件给直接拦截掉,以至于无法准确的获取访客数据。

      这里有大佬已经给出了方案,参考解决 Umami 统计脚本被拦截广告插件拦截 - ROYWANG

  • 点击部署按钮,然后可以访问 <deploy-id>.netlify.app 查看效果。

  • 后续可以在 Site Settings -> Build & Deploy-> Environment variables 编辑环境变量,需要重新部署服务生效

如何使用

接入网站

  • 配置完成网站后打开,使用初始用户名密码登录 admin:umami。登录完成后可在右上角个人资料,更新密码处修改默认信息。接着个人资料,网站,添加网址处接入待统计的网站,如果勾选了启用共享链接,代表可以将该网站的统计数据分享给他人查看。

  • 添加网站后,在网站列表的获取跟踪代码按钮处点击,复制具体内容到标签中,注:如果开启了自定义脚本名称,此处的链接文件需要手动替换。

跟踪事件

Umami 的自定义跟踪事件有些鸡肋,只能做到统计数量。支持两类:一是 CSS 类,也就是 umami--<event>--<event-name> 打标方式;另一种是在加载完成 Umami 文件后,提供了 umami 对象,可以调用一些方法,两种方式其实都挺勉强。

详情见官网文档:https://umami.is/docs/track-events

在 hexo 中使用

如果主题没有适配 umami 或者你的主题不支持插入能够传入参数的js脚本,可以使用 hexo injector 直接注入,hexo 版本需要大于 5

在你的网站根目录下的 Scripts 文件夹下(愣着干嘛,没有新建啊),新建一个 umami.js 文件,复制以下代码进去保存即可。

自行替换掉其中的 <your-website-id><your-umami-url> ,你可以在umami控制台设置中添加网站找到两段代码

hexo.extend.injector.register('head_end', '<script async defer data-website-id="<your-website-id>" src="<your-umami-url>"></script>');

再次 hexo g & hexo d 重现打开你的网站,F12打开控制台,出现 <TRACKER_SCRIPT_NAME>.js 即为引用成功

浏览器屏蔽掉js脚本(推荐)

这一步是可选的,不过我十分建议你这样做。因为Hexo自身的特性,我们在编写完文章后并不能直接预览,必须由hexo渲染出html页面才能在本地预览。这就导致了你在预览你的页面的时候umami的脚本一样会自动加载,意思就是你自己预览自己的页面也会被计入统计,这就有点麻烦,因为umami是没有域名过滤的功能的,所以你分不清哪些是外来访客哪些是你自己预览的,十分的不方便&不直观。

两种方法:

  • 使用浏览器 Fn+12,记录网络活动,找到js脚本右键拦截域

  • 使用浏览器广告屏蔽插件,如 uBlock AdBlock (plus) AdGuard 等,我自己用的是 uBlock,所以以它来举例。 进入后台详细设置,点击自定义静态规则,在下方的编辑框中加入以下代码,自行替换其中<your-doamin> & <your-js-name>

    ||<your-domain>/<your-js-name>.js$script

测试完不要忘记去刷新一下页面看又没有生效

一些题外话

为啥我不用Vercel?

其实并不是没有尝试过使用vercel,只是不知道为啥vercel部署完成后登录的时候login会报Error 500的错误,然后卡在登陆界面没有反应,没弄清楚,Netlify就没有问题

为什么Supabase不用Connection info里面的链接呢?

因为等会儿部署的时候它会报错连不上Datebase,不要参考官方文档中的标准链接,不过貌似在我写这篇文章的时候好像官方文档已经改过来了 umami - Running on Supabase

为什么你的初始化数据库代码与官方的不一样?

因为会报Error: P3018(issue里面好像说是没有读写权限?)
[01:50:57.036] Running "vercel build"
[01:50:57.538] Vercel CLI 28.4.4
[01:50:57.884] Installing dependencies...
[01:50:58.232] yarn install v1.22.17
[01:50:58.314] [1/4] Resolving packages...
[01:50:58.629] [2/4] Fetching packages...
[01:51:19.971] [3/4] Linking dependencies...
[01:51:19.974] warning " > next-basics@0.7.0" has incorrect peer dependency "react@^18.2.0".
[01:51:19.974] warning " > next-basics@0.7.0" has incorrect peer dependency "react-dom@^18.2.0".
[01:51:19.975] warning "react-spring > @react-spring/konva@9.5.2" has unmet peer dependency "konva@>=2.6".
[01:51:19.975] warning "react-spring > @react-spring/konva@9.5.2" has unmet peer dependency "react-konva@^16.8.0 || ^17.0.0".
[01:51:19.976] warning "react-spring > @react-spring/native@9.5.2" has unmet peer dependency "react-native@>=0.58".
[01:51:19.976] warning "react-spring > @react-spring/three@9.5.2" has unmet peer dependency "@react-three/fiber@>=6.0".
[01:51:19.976] warning "react-spring > @react-spring/three@9.5.2" has unmet peer dependency "three@>=0.126".
[01:51:19.976] warning "react-spring > @react-spring/zdog@9.5.2" has unmet peer dependency "react-zdog@>=1.0".
[01:51:19.978] warning "react-spring > @react-spring/zdog@9.5.2" has unmet peer dependency "zdog@>=1.0".
[01:51:19.985] warning "eslint-config-next > @typescript-eslint/parser > @typescript-eslint/typescript-estree > tsutils@3.21.0" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
[01:51:39.133] [4/4] Building fresh packages...
[01:51:46.252] $ prisma generate || true
[01:51:48.580] prisma:warn The postinstall script automatically ran `prisma generate` and did not find your `prisma/schema.prisma`.
[01:51:48.581] If you have a Prisma schema file in a custom path, you will need to run
[01:51:48.581] `prisma generate --schema=./path/to/your/schema.prisma` to generate Prisma Client.
[01:51:48.581] If you do not have a Prisma schema file yet, you can ignore this message.
[01:51:48.581]
[01:51:49.386] $ node -e "if (process.env.NODE_ENV !== 'production'){process.exit(1)} " || husky install
[01:51:49.495] husky - Git hooks installed
[01:51:49.498] Done in 51.27s.
[01:51:49.534] Detected Next.js version: 12.2.5
[01:51:49.536] Running "yarn run build"
[01:51:49.805] yarn run v1.22.17
[01:51:49.849] $ npm-run-all build-db check-db build-tracker build-geo build-app
[01:51:50.293] $ npm-run-all copy-db-files build-db-client
[01:51:50.733] $ node scripts/copy-db-files.js
[01:51:50.867] Database type detected: postgresql
[01:51:50.876] Copied /vercel/path0/db/postgresql to /vercel/path0/prisma
[01:51:51.223] $ prisma generate
[01:51:53.170] Prisma schema loaded from prisma/schema.prisma
[01:51:54.575]
[01:51:54.575] ✔ Generated Prisma Client (4.3.1 | library) to ./node_modules/@prisma/client in 215ms
[01:51:54.576] You can now start using Prisma Client in your code. Reference: https://pris.ly/d/client
[01:51:54.576] ```
[01:51:54.576] import { PrismaClient } from '@prisma/client'
[01:51:54.576] const prisma = new PrismaClient()
[01:51:54.576] ```
[01:51:54.716] ┌─────────────────────────────────────────────────────────┐
[01:51:54.716] │ Update available 4.3.1 -> 4.4.0 │
[01:51:54.717] │ Run the following to update │
[01:51:54.717] │ yarn add --dev prisma@latest │
[01:51:54.717] │ yarn add @prisma/client@latest │
[01:51:54.717] └─────────────────────────────────────────────────────────┘
[01:51:55.090] $ node scripts/check-db.js
[01:51:55.226] ✓ DATABASE_URL is defined.
[01:51:56.406] ✓ Database connection successful.
[01:51:57.801] ✓ Database tables found.
[01:52:02.783] Prisma schema loaded from prisma/schema.prisma
[01:52:02.783] Datasource "db": PostgreSQL database "postgres", schema "public" at "db.wdwzdvdykslszebhocqx.supabase.co:5432"
[01:52:02.783]
[01:52:02.783] 3 migrations found in prisma/migrations
[01:52:02.783]
[01:52:02.783] Following migrations have not yet been applied:
[01:52:02.783] 01_init
[01:52:02.783] 02_add_event_data
[01:52:02.783] 03_remove_casade_delete
[01:52:02.783]
[01:52:02.784] To apply migrations in development run yarn prisma migrate dev.
[01:52:02.784] To apply migrations in production run yarn prisma migrate deploy.
[01:52:02.784]
[01:52:02.784] Running update...
[01:52:08.475] Prisma schema loaded from prisma/schema.prisma
[01:52:08.475] Datasource "db": PostgreSQL database "postgres", schema "public" at "db.wdwzdvdykslszebhocqx.supabase.co:5432"
[01:52:08.475] Migration 01_init marked as applied.
[01:52:08.475]
[01:52:16.565] Error: P3018
[01:52:16.565]
[01:52:16.566] A migration failed to apply. New migrations cannot be applied before the error is recovered from. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve
[01:52:16.566]
[01:52:16.566] Migration name: 02_add_event_data
[01:52:16.566]
[01:52:16.566] Database error code: 42501
[01:52:16.567]
[01:52:16.567] Database error:
[01:52:16.567] ERROR: must be owner of table event
[01:52:16.568]
[01:52:16.568] DbError { severity: "ERROR", parsed_severity: Some(Error), code: SqlState(E42501), message: "must be owner of table event", detail: None, hint: None, position: None, where_: None, schema: None, table: None, column: None, datatype: None, constraint: None, file: Some("aclchk.c"), line: Some(3583), routine: Some("aclcheck_error") }
[01:52:16.568]
[01:52:16.568]
[01:52:16.568] ✗ Command failed: prisma migrate deploy
[01:52:16.569] Error: P3018
[01:52:16.569]
[01:52:16.569] A migration failed to apply. New migrations cannot be applied before the error is recovered from. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve
[01:52:16.569]
[01:52:16.569] Migration name: 02_add_event_data
[01:52:16.569]
[01:52:16.569] Database error code: 42501
[01:52:16.569]
[01:52:16.569] Database error:
[01:52:16.569] ERROR: must be owner of table event
[01:52:16.569]
[01:52:16.569] DbError { severity: "ERROR", parsed_severity: Some(Error), code: SqlState(E42501), message: "must be owner of table event", detail: None, hint: None, position: None, where_: None, schema: None, table: None, column: None, datatype: None, constraint: None, file: Some("aclchk.c"), line: Some(3583), routine: Some("aclcheck_error") }
[01:52:16.570]
[01:52:16.570]
[01:52:16.570]
[01:52:16.583] error Command failed with exit code 1.
[01:52:16.583] info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
[01:52:16.598] ERROR: "check-db" exited with 1.
[01:52:16.611] error Command failed with exit code 1.
[01:52:16.611] info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
[01:52:16.632] Error: Command "yarn run build" exited with 1

可以参考一下issue:

https://github.com/umami-software/umami/issues/1525

https://github.com/umami-software/umami/discussions/1486#discussioncomment-3567397

还有其他的数据库可以部署吗?

当然有,比如PlanetScale,只是不知道为啥同样也是一直报错,没搞懂,读者可以自行摸索