feat: add analytics (#528)

* feat: add analytics

* feat: send data to umami

* refactor: fix format

* fix: click behavior of switches

* refactor: format

* chore: rerun ci

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Manuel
2024-05-26 17:18:25 +02:00
committed by GitHub
parent d57b771a17
commit 7e64d39693
17 changed files with 558 additions and 179 deletions

View File

@@ -0,0 +1 @@
export * from "./src";

View File

@@ -0,0 +1,43 @@
{
"name": "@homarr/analytics",
"private": true,
"version": "0.1.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"license": "MIT",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"lint": "eslint .",
"format": "prettier --check . --ignore-path ../../.gitignore",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@homarr/db": "workspace:^0.1.0",
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^8.57.0",
"typescript": "^5.4.5"
},
"eslintConfig": {
"extends": [
"@homarr/eslint-config/base"
]
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@umami/node": "^0.3.0",
"superjson": "2.2.1"
}
}

View File

@@ -0,0 +1,2 @@
export const UMAMI_HOST_URL = "https://umami.homarr.dev";
export const UMAMI_WEBSITE_ID = "ff7dc470-a84f-4779-b1ab-66a5bb16a94b";

View File

@@ -0,0 +1,2 @@
export * from "./constants";
export * from "./send-server-analytics";

View File

@@ -0,0 +1,102 @@
import type { UmamiEventData } from "@umami/node";
import { Umami } from "@umami/node";
import SuperJSON from "superjson";
import { count, db, eq } from "@homarr/db";
import { integrations, items, serverSettings, users } from "@homarr/db/schema/sqlite";
import { logger } from "@homarr/log";
import type { defaultServerSettings } from "@homarr/server-settings";
import { Stopwatch } from "../../common/src";
import { UMAMI_HOST_URL, UMAMI_WEBSITE_ID } from "./constants";
export const sendServerAnalyticsAsync = async () => {
const stopWatch = new Stopwatch();
const setting = await db.query.serverSettings.findFirst({
where: eq(serverSettings.settingKey, "analytics"),
});
if (!setting) {
logger.info(
"Server does not know the configured state of analytics. No data will be sent. Enable analytics in the settings",
);
return;
}
const analyticsSettings = SuperJSON.parse<typeof defaultServerSettings.analytics>(setting.value);
if (!analyticsSettings.enableGeneral) {
logger.info("Analytics are disabled. No data will be sent. Enable analytics in the settings");
return;
}
const umamiInstance = new Umami();
umamiInstance.init({
hostUrl: UMAMI_HOST_URL,
websiteId: UMAMI_WEBSITE_ID,
});
await sendIntegrationDataAsync(umamiInstance, analyticsSettings);
await sendWidgetDataAsync(umamiInstance, analyticsSettings);
await sendUserDataAsync(umamiInstance, analyticsSettings);
logger.info(`Sent all analytics in ${stopWatch.getElapsedInHumanWords()}`);
};
const sendWidgetDataAsync = async (umamiInstance: Umami, analyticsSettings: typeof defaultServerSettings.analytics) => {
if (!analyticsSettings.enableWidgetData) {
return;
}
const widgetCount = (await db.select({ count: count(items.id) }).from(items))[0]?.count ?? 0;
const response = await umamiInstance.track("server-widget-data", {
countWidgets: widgetCount,
});
if (response.ok) {
return;
}
logger.warn("Unable to send track event data to Umami instance");
};
const sendUserDataAsync = async (umamiInstance: Umami, analyticsSettings: typeof defaultServerSettings.analytics) => {
if (!analyticsSettings.enableUserData) {
return;
}
const userCount = (await db.select({ count: count(users.id) }).from(users))[0]?.count ?? 0;
const response = await umamiInstance.track("server-user-data", {
countUsers: userCount,
});
if (response.ok) {
return;
}
logger.warn("Unable to send track event data to Umami instance");
};
const sendIntegrationDataAsync = async (
umamiInstance: Umami,
analyticsSettings: typeof defaultServerSettings.analytics,
) => {
if (!analyticsSettings.enableIntegrationData) {
return;
}
const integrationKinds = await db
.select({ kind: integrations.kind, count: count(integrations.id) })
.from(integrations)
.groupBy(integrations.kind);
const map: UmamiEventData = {};
integrationKinds.forEach((integrationKind) => {
map[integrationKind.kind] = integrationKind.count;
});
const response = await umamiInstance.track("server-integration-data-kind", map);
if (response.ok) {
return;
}
logger.warn("Unable to send track event data to Umami instance");
};

View File

@@ -0,0 +1,8 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}

View File

@@ -2,13 +2,34 @@ import SuperJSON from "superjson";
import { eq } from "@homarr/db";
import { serverSettings } from "@homarr/db/schema/sqlite";
import type { ServerSettings } from "@homarr/server-settings";
import { logger } from "@homarr/log";
import type { defaultServerSettings, ServerSettings } from "@homarr/server-settings";
import { defaultServerSettingsKeys } from "@homarr/server-settings";
import { z } from "@homarr/validation";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
export const serverSettingsRouter = createTRPCRouter({
// this must be public so anonymous users also get analytics
getAnalytics: publicProcedure.query(async ({ ctx }) => {
const setting = await ctx.db.query.serverSettings.findFirst({
where: eq(serverSettings.settingKey, "analytics"),
});
if (!setting) {
logger.info(
"Server settings for analytics is currently undefined. Using default values instead. If this persists, there may be an issue with the server settings",
);
return {
enableGeneral: true,
enableIntegrationData: false,
enableUserData: false,
enableWidgetData: false,
} as (typeof defaultServerSettings)["analytics"];
}
return SuperJSON.parse<(typeof defaultServerSettings)["analytics"]>(setting.value);
}),
getAll: protectedProcedure.query(async ({ ctx }) => {
const settings = await ctx.db.query.serverSettings.findMany();