使用 Next.js、Tinybird 和 Tremor 构建实时分析仪表板:综合指南
您将在本文中发现什么?
Papermark - 开源的 DocSend 替代品。
设置项目
构建应用程序
结论
帮帮我!
您将在本文中发现什么?
实时分析已成为当今应用程序的重要组成部分,尤其是在理解用户行为并基于数据驱动的洞察改进平台方面。如果您的用户依赖您的产品来提供洞察,那么实时分析就显得尤为重要。
在本文中,我们将探讨如何使用 Tinybird、Tremor.so 和 Next.js 创建页面浏览量的实时分析仪表板。
我们将使用 Tinybird 进行数据提取和实时数据分析,使用 Tremor 进行数据可视化,并使用 Next.js 进行服务器端渲染。
Papermark - 开源的 DocSend 替代品。
在开始之前,我先来介绍一下 Papermark。它是一个开源项目,用于安全地共享文档,内置实时逐页分析功能,由 Tinybird 提供支持,并由 Tremor 进行可视化。
如果你能给我们一颗星,我会非常高兴!别忘了在评论区分享你的想法❤️
https://github.com/mfts/papermark
设置项目
让我们设置项目环境。我们将设置一个 Next.js 应用,安装 Tinybird CLI,并配置所需的服务和工具。
准备茶
手边备有一个包管理器是个好主意,比如tea
。它可以处理你的开发环境,简化你的(编程)生活!
sh <(curl https://tea.xyz)
# --- OR ---
# using brew
brew install teaxyz/pkgs/tea-cli
tea
让您可以专注于代码,因为它会负责安装python
和pipenv
(我用它来运行tinybird-cli
)、node
以及npm
您可能需要的任何其他软件包。最棒的是,它tea
会将所有软件包安装在一个专用目录中(默认值~/.tea
:),让您的系统文件保持整洁。
使用 TypeScript 和 Tailwindcss 设置 Next.js
我们将使用 create-next-app 生成一个新的 Next.js 项目。我们还将使用 TypeScript 和 Tailwind CSS,因此请确保在出现提示时选择这些选项。
npx create-next-app
# ---
# you'll be asked the following prompts
What is your project named? my-app
Would you like to add TypeScript with this project? Y/N
# select `Y` for typescript
Would you like to use ESLint with this project? Y/N
# select `Y` for ESLint
Would you like to use Tailwind CSS with this project? Y/N
# select `Y` for Tailwind CSS
Would you like to use the `src/ directory` with this project? Y/N
# select `N` for `src/` directory
What import alias would you like configured? `@/*`
# enter `@/*` for import alias
安装 Tinybird
Tinybird 的命令行界面 (CLI) 可以帮助我们管理数据源和管道。我正在tinybird-cli
使用进行安装pipenv
,它在我们的本地环境中为我的 pip 包管理一个虚拟环境。
# Navigate to your Next.js repo
cd my-app
# Create a new virtual environment and install Tinybird CLI
# if you install `tea` in the previous step, then tea will take care of installing pipenv and its dependencies
pipenv install tinybird-cli
# Activate the virtual environment
pipenv shell
配置 Tinybird
前往tinybird.co创建一个免费账户。您需要拥有一个 Tinybird 账户和一个关联的代币。您可以从 Tinybird 的仪表盘获取代币。
# Authenticate with tinybird-cli using your auth token when prompted
tb auth
一个.tinyb
文件将被添加到你的仓库根目录。无需修改。但请将其添加到你的.gitignore
文件中,以避免暴露你的令牌。
echo ".tinyb" >> .gitignore
构建应用程序
现在我们已经完成了设置,可以开始构建应用程序了。我们将介绍的主要功能包括:
- Tinybird 管道和数据源
- 页面浏览记录
- 震颤条形图
#1 Tinybird 管道和数据源
Tinybird 能够以编程方式配置管道和数据源,这带来了显著的优势。这种灵活性使我们能够将数据基础设施视为代码,这意味着整个配置都可以提交到版本控制系统中。对于像 Papermark 这样的开源项目来说,这种功能非常有益。它促进了透明度和协作,因为贡献者可以轻松理解数据结构,而不会产生任何歧义。
我们按如下方式设置 Tinybird 管道和数据源:
mkdir -p lib/tinybird/{datasources,endpoints}
1. 数据源
这基本上是一个版本化的架构page_views
。这是我们提取、存储和读取页面浏览量分析数据所需的唯一数据源。您可以随意添加/删除字段。
# lib/tinybird/datasources/page_views.datasource
VERSION 1
DESCRIPTION >
Page views are events when a user views a document
SCHEMA >
`id` String `json:$.id`,
`linkId` String `json:$.linkId`,
`documentId` String `json:$.documentId`,
`viewId` String `json:$.viewId`,
# Unix timestamp
`time` Int64 `json:$.time`,
`duration` UInt32 `json:$.duration`,
# The page number
`pageNumber` LowCardinality(String) `json:$.pageNumber`,
`country` String `json:$.country`,
`city` String `json:$.city`,
`region` String `json:$.region`,
`latitude` String `json:$.latitude`,
`longitude` String `json:$.longitude`,
`ua` String `json:$.ua`,
`browser` String `json:$.browser`,
`browser_version` String `json:$.browser_version`,
`engine` String `json:$.engine`,
`engine_version` String `json:$.engine_version`,
`os` String `json:$.os`,
`os_version` String `json:$.os_version`,
`device` String `json:$.device`,
`device_vendor` String `json:$.device_vendor`,
`device_model` String `json:$.device_model`,
`cpu_architecture` String `json:$.cpu_architecture`,
`bot` UInt8 `json:$.bot`,
`referer` String `json:$.referer`,
`referer_url` String `json:$.referer_url`
ENGINE "MergeTree"
ENGINE_SORTING_KEY "linkId,documentId,viewId,pageNumber,time,id"
2.管道
在 Tinybird 中,管道是应用于数据的一系列转换操作。这些转换可能包括从简单的数据清理操作到复杂的聚合和分析操作。
我们有一个版本化的管道(有时也称为端点)来检索 page_view 数据。这个名为 的 Tinybird 管道endpoint
会计算用户在特定时间范围内浏览特定文档每页的平均时长,并按页码升序显示结果。
# lib/endpoints/get_average_page_duration.pipe
VERSION 1
NODE endpoint
SQL >
%
SELECT
pageNumber,
AVG(duration) AS avg_duration
FROM
page_views__v1
WHERE
documentId = {{ String(documentId, required=True) }}
AND time >= {{ Int64(since, required=True) }}
GROUP BY
pageNumber
ORDER BY
pageNumber ASC
现在我们已经设置了 Tinybird 数据源和管道,您需要使用 CLI 将它们推送到您的 Tinybird 帐户。
# Navigate to the directory containing your datasource and pipe files
cd lib/tinybird
# Push your files to Tinybird
tb push datasources/*.datasource pipes/*.pipe
3. Typescript 函数
让我们设置适当的 TypeScript 函数来实际发送和检索 Tinybird 的数据。我们使用zod
chronark 的zod-bird
库
此函数用于从 Tinybird检索数据:
// lib/tinybird/pipes.ts
import { z } from "zod";
import { Tinybird } from "@chronark/zod-bird";
const tb = new Tinybird({ token: process.env.TINYBIRD_TOKEN! });
export const getTotalAvgPageDuration = tb.buildPipe({
pipe: "get_total_average_page_duration__v1",
parameters: z.object({
documentId: z.string(),
since: z.number(),
}),
data: z.object({
pageNumber: z.string(),
avg_duration: z.number(),
}),
});
此函数用于向 Tinybird发送数据:
// lib/tinybird/publish.ts
import { z } from "zod";
import { Tinybird } from "@chronark/zod-bird";
const tb = new Tinybird({ token: process.env.TINYBIRD_TOKEN! });
export const publishPageView = tb.buildIngestEndpoint({
datasource: "page_views__v1",
event: z.object({
id: z.string(),
linkId: z.string(),
documentId: z.string(),
viewId: z.string(),
time: z.number().int(),
duration: z.number().int(),
pageNumber: z.string(),
country: z.string().optional().default("Unknown"),
city: z.string().optional().default("Unknown"),
region: z.string().optional().default("Unknown"),
latitude: z.string().optional().default("Unknown"),
longitude: z.string().optional().default("Unknown"),
ua: z.string().optional().default("Unknown"),
browser: z.string().optional().default("Unknown"),
browser_version: z.string().optional().default("Unknown"),
engine: z.string().optional().default("Unknown"),
engine_version: z.string().optional().default("Unknown"),
os: z.string().optional().default("Unknown"),
os_version: z.string().optional().default("Unknown"),
device: z.string().optional().default("Desktop"),
device_vendor: z.string().optional().default("Unknown"),
device_model: z.string().optional().default("Unknown"),
cpu_architecture: z.string().optional().default("Unknown"),
bot: z.boolean().optional(),
referer: z.string().optional().default("(direct)"),
referer_url: z.string().optional().default("(direct)"),
}),
});
4. 配置生产环境的 Auth Token
别忘了添加TINYBIRD_TOKEN
到你的.env
文件中。建议你创建一个具有最小操作范围的令牌:
- 从特定管道读取
- 附加到特定数据源
恭喜!🎉 您已成功配置 Tinybird,可以进入下一步了。
#2 页面浏览记录
我们将在 PDFviewer 组件中捕获页面查看事件并将其发布到 Tinybird 的数据源。
让我们构建一个 API 函数,以便在每次浏览页面时将数据发送到 Tinybird:
// pages/api/record_view.ts
import { NextApiRequest, NextApiResponse } from "next";
import { publishPageView } from "@/lib/tinybird";
import { z } from "zod";
import { v4 as uuidv4 } from 'uuid';
// Define the validation schema
const bodyValidation = z.object({
id: z.string(),
linkId: z.string(),
documentId: z.string(),
viewId: z.string(),
time: z.number().int(),
duration: z.number().int(),
pageNumber: z.string(),
...
});
export default async function handle(
req: NextApiRequest,
res: NextApiResponse
) {
// We only allow POST requests
if (req.method !== "POST") {
res.status(405).json({ message: "Method Not Allowed" });
return;
}
const { linkId, documentId, viewId, duration, pageNumber } = req.body;
const time = Date.now(); // in milliseconds
const pageViewId = uuidv4();
const pageViewObject = {
id: pageViewId,
linkId,
documentId,
viewId,
time,
duration,
pageNumber: pageNumber.toString(),
...
};
const result = bodyValidation.safeParse(pageViewObject);
if (!result.success) {
return res.status(400).json(
{ error: `Invalid body: ${result.error.message}` }
);
}
try {
await publishPageView(result.data);
res.status(200).json({ message: "View recorded" });
} catch (error) {
res.status(500).json({ message: (error as Error).message });
}
}
最后是发送请求的 PDFViewer 组件:
// components/PDFViewer.tsx
import { useState, useEffect } from 'react';
const PDFViewer = () => {
const [pageNumber, setPageNumber] = useState<number>(1)
useEffect(() => {
startTime = Date.now(); // update the start time for the new page
// when component unmounts, calculate duration and track page view
return () => {
const endTime = Date.now();
const duration = Math.round(endTime - startTime);
trackPageView(duration);
};
}, [pageNumber]); // monitor pageNumber for changes
async function trackPageView(duration: number = 0) {
await fetch("/api/record_view", {
method: "POST",
body: JSON.stringify({
linkId: props.linkId,
documentId: props.documentId,
viewId: props.viewId,
duration: duration,
pageNumber: pageNumber,
}),
headers: {
"Content-Type": "application/json",
},
});
}
return (
// Your PDF Viewer implementation
);
}
export default PDFViewer;
#3 震颤条形图
现在让我们创建一个条形图来显示每个文档的页面浏览量。我们使用tremor.so来构建我们漂亮的仪表板。
# Install tremor with their CLI
npx @tremor/cli@latest init
// components/bar-chart.tsx
import { BarChart } from "@tremor/react";
const timeFormatter = (number) => {
const totalSeconds = Math.floor(number / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.round(totalSeconds % 60);
// Adding zero padding if seconds less than 10
const secondsFormatted = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${minutes}:${secondsFormatted}`;
};
export default function BarChartComponent({data}) {
return (
<BarChart
className="mt-6 rounded-tremor-small"
data={data}
index="pageNumber"
categories={["Time spent per page"]}
colors={["gray"]}
valueFormatter={timeFormatter}
yAxisWidth={50}
showGridLines={false}
/>
);
}
// lib/swr/use-stats.ts
import { useRouter } from "next/router";
import useSWR from "swr";
import { getTotalAvgPageDuration } from "@/lib/tinybird/pipes";
export function useStats() {
const router = useRouter();
const { id } = router.query as { id: string };
const { data, error } = useSWR(
id,
() => getTotalAvgPageDuration({ documentId: id, since: 0
}),
{
dedupingInterval: 10000,
}
);
return {
durationData: data,
isLoading: !error && !data,
error,
};
}
// pages/document/[id].tsx
import { useDocument } from "@/lib/swr/use-document";
import { useStats } from "@/lib/swr/use-stats";
import BarChartComponent from "@/components/bar-chart";
export default function DocumentPage() {
const { document, error: documentError } = useDocument();
const { stats, error: statsError } = useStats();
if (documentError) {
// handle document error
}
if (statsError) {
// handle stats error
}
return (
<>
<main>
{document && (
<header>
<h1>{document.name}</h1>
{stats && <BarChartComponent data={stats.durationData} />}
</header>
)}
</main>
</>
);
}
瞧!🎉 带有精确逐页时间的条形图!
结论
就这样!我们使用 Tinybird、Tremor 和 Next.js 构建了一个用于页面浏览量的实时分析仪表盘。虽然这里的示例很简单,但相同的概念可以扩展,以处理您的应用可能需要执行的任何类型的分析。
感谢您的阅读。我是 Marc,一位开源倡导者。我正在开发papermark.com ——DocSend 的开源替代方案,提供毫秒级精确的页面分析功能。
帮帮我!
如果您觉得这篇文章对您有帮助,并且能够更好地理解 Tinybird 以及 Tremor 的仪表盘功能,请给我们一颗星,我将不胜感激!也别忘了在评论区分享您的想法 ❤️
https://github.com/mfts/papermark
链接:https://dev.to/mfts/building-a-real-time-analytics-dashboard-with-nextjs-tinybird-and-tremor-a-comprehensive-guide-15k0