mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user