feat(analytics): build-time bake + telemetry depth (#336)

Bake PostHog + Sentry into the published Docker image (SNAPOTTER_ANALYTICS
build arg, codegen script). Delete entire consent system. Move event emission
to BullMQ worker. Add cross-tier identity stitching, Sentry performance
tracing on both tiers, frontend funnel events. Fix stateful regex bug.

86 files changed, 1593 insertions(+), 3747 deletions(-)
This commit is contained in:
SnapOtter
2026-06-24 11:05:39 +08:00
committed by GitHub
parent a53038ed96
commit 5d36ac06d8
86 changed files with 1594 additions and 3748 deletions
@@ -1,78 +0,0 @@
import { Shield } from "lucide-react";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "@/contexts/i18n-context";
import { useAnalyticsStore } from "@/stores/analytics-store";
export function AnalyticsConsentPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const { config, configLoaded, fetchConfig, acceptAnalytics, declineAnalytics, remindLater } =
useAnalyticsStore();
useEffect(() => {
fetchConfig();
}, [fetchConfig]);
useEffect(() => {
if (configLoaded && !config?.enabled) {
declineAnalytics().then(() => navigate("/", { replace: true }));
}
}, [configLoaded, config, navigate, declineAnalytics]);
if (!configLoaded) {
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="h-8 w-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
);
}
const handleAccept = async () => {
await acceptAnalytics();
window.location.href = "/";
};
const handleDecline = async () => {
await remindLater();
window.location.href = "/";
};
return (
<div className="flex min-h-screen items-center justify-center bg-background p-6">
<div className="w-full max-w-[400px] space-y-6 rounded-2xl border border-border bg-card p-9 shadow-lg">
<div className="flex justify-center">
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-primary/10">
<Shield className="h-[22px] w-[22px] text-primary" />
</div>
</div>
<div className="space-y-3 text-center">
<h1 className="text-lg font-semibold text-foreground">{t.analytics.consentTitle}</h1>
<p className="text-sm leading-relaxed text-muted-foreground">
{t.analytics.consentDescription}
</p>
</div>
<p className="text-center text-xs text-muted-foreground">{t.analytics.consentChangeable}</p>
<div className="flex gap-2.5">
<button
type="button"
onClick={handleAccept}
className="flex-1 rounded-[10px] bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
{t.analytics.acceptButton}
</button>
<button
type="button"
onClick={handleDecline}
className="flex-1 rounded-[10px] border border-border px-4 py-2.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted"
>
{t.analytics.declineButton}
</button>
</div>
</div>
</div>
);
}
+11 -1
View File
@@ -1,5 +1,5 @@
import type { Tool } from "@snapotter/shared";
import { CATEGORIES, SECTIONS, TOOLS, toolSection } from "@snapotter/shared";
import { ANALYTICS_EVENTS, CATEGORIES, SECTIONS, TOOLS, toolSection } from "@snapotter/shared";
import { ChevronDown, Search, X } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
@@ -85,6 +85,16 @@ export function HomePage() {
const searchResults = useFuseSearch(visibleTools, search);
useEffect(() => {
if (!search || search.length < 2) return;
const timer = setTimeout(() => {
import("@/lib/analytics").then(({ track }) => {
track(ANALYTICS_EVENTS.SEARCH, { query: search, results_count: searchResults.length });
});
}, 1000);
return () => clearTimeout(timer);
}, [search, searchResults.length]);
const tabTools = useMemo(() => {
if (activeTab === "all") return visibleTools;
return visibleTools.filter((tool) => toolSection(tool) === activeTab);
+8
View File
@@ -1,4 +1,5 @@
import {
ANALYTICS_EVENTS,
getRequiredBundlesForTool,
PYTHON_SIDECAR_TOOLS,
SECTIONS,
@@ -263,6 +264,13 @@ export function ToolPage() {
useEffect(() => {
if (tool) {
recordRecentTool(tool.id);
import("@/lib/analytics").then(({ track }) => {
track(ANALYTICS_EVENTS.TOOL_OPENED, {
tool_id: tool.id,
modality: tool.modality,
category: tool.category,
});
});
}
}, [tool]);