diff --git a/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx b/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx index 08e3e0c2d..3badc014e 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx @@ -1,6 +1,5 @@ "use client"; -import { startTransition } from "react"; import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core"; import { IconArrowRight } from "@tabler/icons-react"; import type { z } from "zod/v4"; @@ -24,16 +23,6 @@ export const InitSettings = () => { }); const form = useZodForm(settingsInitSchema, { initialValues: defaultServerSettings }); - form.watch("analytics.enableGeneral", ({ value }) => { - if (!value) { - startTransition(() => { - form.setFieldValue("analytics.enableWidgetData", false); - form.setFieldValue("analytics.enableIntegrationData", false); - form.setFieldValue("analytics.enableUserData", false); - }); - } - }); - const handleSubmitAsync = async (values: z.infer) => { await mutateAsync(values); }; @@ -47,24 +36,6 @@ export const InitSettings = () => { - - - - - - diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/analytics.settings.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/analytics.settings.tsx index 21215e6d0..568ab93a7 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/analytics.settings.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/analytics.settings.tsx @@ -24,15 +24,6 @@ export const AnalyticsSettings = ({ initialData }: AnalyticsSettingsProps) => { return; } - if ( - !updatedValues.enableGeneral && - (updatedValues.enableWidgetData || updatedValues.enableIntegrationData || updatedValues.enableUserData) - ) { - updatedValues.enableIntegrationData = false; - updatedValues.enableUserData = false; - updatedValues.enableWidgetData = false; - } - void mutateAsync({ settingsKey: "analytics", value: updatedValues, @@ -54,30 +45,6 @@ export const AnalyticsSettings = ({ initialData }: AnalyticsSettingsProps) => { - - - diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 18324242c..d6728ee9c 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -22,8 +22,11 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { + "@homarr/auth": "workspace:^0.1.0", + "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", + "@homarr/docker": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", "@umami/node": "^0.4.0", "superjson": "2.2.6" diff --git a/packages/analytics/src/send-server-analytics.ts b/packages/analytics/src/send-server-analytics.ts index 27f1ae3d8..2557e3ca6 100644 --- a/packages/analytics/src/send-server-analytics.ts +++ b/packages/analytics/src/send-server-analytics.ts @@ -1,17 +1,52 @@ import type { UmamiEventData } from "@umami/node"; import { Umami } from "@umami/node"; +import { isProviderEnabled } from "@homarr/auth/server"; +import { Stopwatch } from "@homarr/common"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { count, db } from "@homarr/db"; +import { isMysql, isPostgresql } from "@homarr/db/collection"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; -import { integrations, items, users } from "@homarr/db/schema"; -import type { defaultServerSettings } from "@homarr/server-settings"; +import { + apiKeys, + apps, + boards, + cronJobConfigurations, + groups, + iconRepositories, + icons, + integrations, + invites, + itemLayouts, + items, + layouts, + medias, + searchEngines, + sectionLayouts, + sections, + users, +} from "@homarr/db/schema"; +import { env as dockerEnv } from "@homarr/docker/env"; -import { Stopwatch } from "../../common/src"; +import packageJson from "../../../package.json"; import { UMAMI_HOST_URL, UMAMI_WEBSITE_ID } from "./constants"; const logger = createLogger({ module: "analytics" }); +const getDatabaseType = (): "mysql" | "postgresql" | "sqlite" => { + if (isMysql()) return "mysql"; + if (isPostgresql()) return "postgresql"; + return "sqlite"; +}; + +const getEnabledAuthProviders = (): string[] => { + const providers: string[] = []; + if (isProviderEnabled("credentials")) providers.push("credentials"); + if (isProviderEnabled("oidc")) providers.push("oidc"); + if (isProviderEnabled("ldap")) providers.push("ldap"); + return providers; +}; + export const sendServerAnalyticsAsync = async () => { const stopWatch = new Stopwatch(); const analyticsSettings = await getServerSettingByKeyAsync(db, "analytics"); @@ -27,67 +62,48 @@ export const sendServerAnalyticsAsync = async () => { websiteId: UMAMI_WEBSITE_ID, }); - await sendIntegrationDataAsync(umamiInstance, analyticsSettings); - await sendWidgetDataAsync(umamiInstance, analyticsSettings); - await sendUserDataAsync(umamiInstance, analyticsSettings); + const enabledAuthProviders = getEnabledAuthProviders(); - logger.info(`Sent all analytics in ${stopWatch.getElapsedInHumanWords()}`); -}; + const analyticsData: UmamiEventData = { + homarrVersion: packageJson.version, + databaseType: getDatabaseType(), + dockerEnabled: dockerEnv.ENABLE_DOCKER ? 1 : 0, + kubernetesEnabled: dockerEnv.ENABLE_KUBERNETES ? 1 : 0, + authCredentials: enabledAuthProviders.includes("credentials") ? 1 : 0, + authOidc: enabledAuthProviders.includes("oidc") ? 1 : 0, + authLdap: enabledAuthProviders.includes("ldap") ? 1 : 0, + countUsers: await db.$count(users), + countBoards: await db.$count(boards), + countGroups: await db.$count(groups), + countApps: await db.$count(apps), + countWidgets: await db.$count(items), + countSections: await db.$count(sections), + countIntegrations: await db.$count(integrations), + countSearchEngines: await db.$count(searchEngines), + countIconRepositories: await db.$count(iconRepositories), + countIcons: await db.$count(icons), + countApiKeys: await db.$count(apiKeys), + countInvites: await db.$count(invites), + countMedias: await db.$count(medias), + countLayouts: await db.$count(layouts), + countItemLayouts: await db.$count(itemLayouts), + countSectionLayouts: await db.$count(sectionLayouts), + countCronJobConfigs: await db.$count(cronJobConfigurations), + }; -const sendWidgetDataAsync = async (umamiInstance: Umami, analyticsSettings: typeof defaultServerSettings.analytics) => { - if (!analyticsSettings.enableWidgetData) { - return; - } - const widgetCount = await db.$count(items); - - 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.$count(users); - - 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; + analyticsData[`integration_${integrationKind.kind}`] = integrationKind.count; }); - const response = await umamiInstance.track("server-integration-data-kind", map); - if (response.ok) { - return; + const response = await umamiInstance.track("server-analytics-v2", analyticsData); + if (!response.ok) { + logger.warn("Unable to send analytics to Umami instance"); } - logger.warn("Unable to send track event data to Umami instance"); + logger.info(`Sent analytics in ${stopWatch.getElapsedInHumanWords()}`); }; diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 6fbbfa187..ef354baaf 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -3305,7 +3305,7 @@ "title": "Analytics", "general": { "title": "Send anonymous analytics", - "text": "Homarr will send anonymized analytics using the open source software Umami. It never collects any personal information and is therefore fully GDPR & CCPA compliant. We encourage you to enable analytics because it helps our open source team to identify issues and prioritize our backlog." + "text": "Homarr will send anonymized analytics using the open source software Umami. When enabled, we collect: Homarr version, database type (MySQL/PostgreSQL/SQLite), Docker/Kubernetes status, authentication methods enabled, and the number of each feature. All data is anonymous and aggregated - no personal information, URLs, names, or identifiable data is ever collected. This helps our open source team understand usage patterns and prioritize development." }, "widgetData": { "title": "Widget data", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1ef280aa..22403284f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -535,12 +535,21 @@ importers: packages/analytics: dependencies: + '@homarr/auth': + specifier: workspace:^0.1.0 + version: link:../auth + '@homarr/common': + specifier: workspace:^0.1.0 + version: link:../common '@homarr/core': specifier: workspace:^0.1.0 version: link:../core '@homarr/db': specifier: workspace:^0.1.0 version: link:../db + '@homarr/docker': + specifier: workspace:^0.1.0 + version: link:../docker '@homarr/server-settings': specifier: workspace:^0.1.0 version: link:../server-settings @@ -11227,7 +11236,7 @@ snapshots: '@babel/code-frame@7.26.2': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -11274,10 +11283,10 @@ snapshots: '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.6 '@babel/template': 7.27.2 '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/types': 7.28.6 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -11317,8 +11326,8 @@ snapshots: '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.28.3 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 '@jridgewell/gen-mapping': 0.3.12 '@jridgewell/trace-mapping': 0.3.29 jsesc: 3.0.2 @@ -11367,7 +11376,7 @@ snapshots: dependencies: '@babel/core': 7.26.0 '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color @@ -11422,11 +11431,11 @@ snapshots: '@babel/parser@7.28.3': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.6 '@babel/parser@7.28.4': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.6 '@babel/parser@7.28.6': dependencies: @@ -11485,9 +11494,9 @@ snapshots: '@babel/code-frame': 7.27.1 '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.6 '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.6 debug: 4.4.3 transitivePeerDependencies: - supports-color