Brings the semantically-correct unification work from the closed agent/release-v2-experience PR into release/v2: - universal create surface (header, board, spotlight) with permission-aware actions - board empty state and setup checklist with widget/connection recipes - in-widget integration editing via inspector modal, preserving unsaved widget settings - single-source widget-integration capability map (nativeFeatureCapabilities) - multi-endpoint Docker singleton with per-endpoint failure isolation, DOCKER_ENDPOINTS descriptors and service reconciliation - Kubernetes multi-context registry with availability probing and context selector - response-contract test harness and setup analytics property boundaries Excludes the feature-platform generator/workbench tooling (deferred).
54 lines
1.7 KiB
TypeScript
54 lines
1.7 KiB
TypeScript
import { TRPCError } from "@trpc/server";
|
|
import z from "zod/v4";
|
|
|
|
import { sendServerAnalyticsAsync, trackEvent } from "@homarr/analytics";
|
|
import { env } from "@homarr/common/env";
|
|
import { db } from "@homarr/db";
|
|
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
|
|
|
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../trpc";
|
|
import { getTrackedFeatureProperties } from "./analytics-properties";
|
|
|
|
const getInstanceId = async (): Promise<string | null> => {
|
|
if (env.NO_EXTERNAL_CONNECTION) return null;
|
|
const settings = await getServerSettingByKeyAsync(db, "analytics");
|
|
if (!settings.enableGeneral) return null;
|
|
return settings.instanceId;
|
|
};
|
|
|
|
export const analyticsRouter = createTRPCRouter({
|
|
sendAnalytics: permissionRequiredProcedure.requiresPermission("admin").mutation(async () => {
|
|
if (env.NO_EXTERNAL_CONNECTION) {
|
|
throw new TRPCError({ code: "PRECONDITION_FAILED", message: "External connections are disabled" });
|
|
}
|
|
return { status: await sendServerAnalyticsAsync() };
|
|
}),
|
|
|
|
trackFeature: protectedProcedure
|
|
.input(
|
|
z.object({
|
|
feature: z.string().max(128),
|
|
properties: z.record(z.string(), z.unknown()).optional(),
|
|
}),
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const instanceId = await getInstanceId();
|
|
if (!instanceId) return;
|
|
|
|
trackEvent(
|
|
instanceId,
|
|
`feature:${input.feature}`,
|
|
getTrackedFeatureProperties(input.feature, input.properties, ctx.session.user.id),
|
|
);
|
|
}),
|
|
|
|
heartbeat: protectedProcedure.mutation(async ({ ctx }) => {
|
|
const instanceId = await getInstanceId();
|
|
if (!instanceId) return;
|
|
|
|
trackEvent(instanceId, "user-active", {
|
|
userId: ctx.session.user.id,
|
|
});
|
|
}),
|
|
});
|