feat(analytics): integrate Rybbit Analytics with error tracking and session replay

- Added rewrites in next.config.mjs for Rybbit Analytics to bypass adblockers.
- Implemented ErrorBoundary and ErrorTracking components in layout.tsx for enhanced error handling.
- Included a script for analytics tracking in the root layout.
- Updated NutritionGrid in product page to handle potential undefined nutrition data.
This commit is contained in:
Evandro Fonseca Junior committed 2026-01-20 20:13:29 -03:00
1 parent c99c82c837
commit 0eeb63bbde
6 files changed
+188 -7

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
"use client";
import React, { Component, ErrorInfo, ReactNode } from "react";
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
/**
* Error Boundary para capturar erros React e enviar ao Rybbit
*/
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Enviar erro ao Rybbit se disponível
if (typeof window !== "undefined" && (window as any).rybbit) {
try {
(window as any).rybbit.track("error", {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
errorName: error.name,
timestamp: new Date().toISOString(),
});
} catch (trackingError) {
console.error("[ErrorBoundary] Falha ao enviar erro ao Rybbit:", trackingError);
}
}
console.error("[ErrorBoundary] Erro capturado:", error, errorInfo);
}
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="min-h-screen bg-white flex items-center justify-center p-6 dark:bg-[#030712]">
<div className="max-w-md w-full text-center space-y-6 border border-slate-200 p-10 rounded-[2.5rem] bg-slate-50 dark:bg-red-950/10 dark:border-red-900/20">
<h1 className="text-2xl font-black text-slate-900 dark:text-white uppercase tracking-tighter">
Erro Inesperado
</h1>
<p className="text-slate-600 text-sm dark:text-slate-400 font-medium">
{this.state.error?.message || "Algo deu errado. Por favor, recarregue a página."}
</p>
<button
onClick={() => {
this.setState({ hasError: false, error: null });
window.location.reload();
}}
className="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors"
>
Recarregar Página
</button>
</div>
</div>
);
}
return this.props.children;
}
}
+62
View File
@@ -0,0 +1,62 @@
"use client";
import { useEffect } from "react";
/**
* Componente para configurar error tracking global do Rybbit
* Captura erros JavaScript não tratados e Promise rejections
*/
export function ErrorTracking() {
useEffect(() => {
// Handler para erros JavaScript não capturados
const handleError = (event: ErrorEvent) => {
if (typeof window !== "undefined" && (window as any).rybbit) {
try {
(window as any).rybbit.track("error", {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
errorName: event.error?.name,
timestamp: new Date().toISOString(),
type: "javascript_error",
});
} catch (trackingError) {
console.error("[ErrorTracking] Falha ao enviar erro ao Rybbit:", trackingError);
}
}
};
// Handler para Promise rejections não tratadas
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
if (typeof window !== "undefined" && (window as any).rybbit) {
try {
const error = event.reason instanceof Error ? event.reason : new Error(String(event.reason));
(window as any).rybbit.track("error", {
message: error.message || String(event.reason),
stack: error.stack,
errorName: error.name || "UnhandledPromiseRejection",
timestamp: new Date().toISOString(),
type: "unhandled_promise_rejection",
});
} catch (trackingError) {
console.error("[ErrorTracking] Falha ao enviar rejection ao Rybbit:", trackingError);
}
}
};
// Adicionar listeners
window.addEventListener("error", handleError);
window.addEventListener("unhandledrejection", handleUnhandledRejection);
// Cleanup
return () => {
window.removeEventListener("error", handleError);
window.removeEventListener("unhandledrejection", handleUnhandledRejection);
};
}, []);
return null;
}
+16 -1
View File
@@ -1,7 +1,10 @@
import "./globals.css";
import { Metadata } from "next";
import { Inter } from "next/font/google";
import Script from "next/script";
import { ThemeProvider } from "@/app/providers";
import { ErrorBoundary } from "@/app/components/ErrorBoundary";
import { ErrorTracking } from "@/app/components/ErrorTracking";
// Use Inter como fallback já que é uma fonte similar à Geist
const inter = Inter({ subsets: ["latin"] });
@@ -79,7 +82,19 @@ export default function RootLayout({
return (
<html lang="pt-BR" suppressHydrationWarning className={inter.className}>
<body className="min-h-screen bg-background antialiased">
<ThemeProvider>{children}</ThemeProvider>
<ErrorBoundary>
<ThemeProvider>
{children}
<ErrorTracking />
</ThemeProvider>
</ErrorBoundary>
<Script
src="/analytics/script.js"
data-site-id="undefined"
data-replay="true"
data-error-tracking="true"
strategy="afterInteractive"
/>
</body>
</html>
);
+1 -1
View File
@@ -173,7 +173,7 @@ export default function ProdutoPage() {
<div className="w-2 h-6 bg-blue-600 rounded-full" />
Composição Nutricional <span className="text-slate-400 font-mono text-xs font-normal">/ 100g</span>
</h3>
<NutritionGrid nutritionData={formatNutritionData(product)} />
<NutritionGrid nutritionData={formatNutritionData(product) || {}} />
</div>
</motion.div>
+26
View File
@@ -25,6 +25,32 @@ const nextConfig = {
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// Proxy reverso para bypass de adblockers (Rybbit Analytics)
// Inclui suporte para session replay e error tracking
async rewrites() {
const rybbitHost = process.env.NEXT_PUBLIC_RYBBIT_HOST || "https://analytics.evandro.dev.br/api";
return [
{
source: "/analytics/:match*",
destination: `${rybbitHost}/:match*`,
},
// Endpoints específicos para garantir funcionamento de replay e error tracking
{
source: "/analytics/v1/replay",
destination: `${rybbitHost}/v1/replay`,
},
{
source: "/analytics/v1/vitals",
destination: `${rybbitHost}/v1/vitals`,
},
{
source: "/analytics/v1/errors",
destination: `${rybbitHost}/v1/errors`,
},
];
},
// Headers de segurança e performance
async headers() {
return [
+5 -5
View File
@@ -5,12 +5,12 @@
"scripts": {
"dev": "next dev",
"lint": "next lint",
"postinstall": "./node_modules/.bin/prisma generate",
"build": "./node_modules/.bin/prisma generate && ./node_modules/.bin/prisma migrate deploy && next build",
"postinstall": "prisma generate",
"build": "prisma generate && prisma migrate deploy && next build",
"start": "next start",
"db:push": "./node_modules/.bin/prisma db push",
"db:generate": "./node_modules/.bin/prisma generate",
"db:migrate": "./node_modules/.bin/prisma migrate deploy"
"db:push": "prisma db push",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate deploy"
},
"dependencies": {
"@prisma/client": "^5.22.0",