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:
1 parent
c99c82c837
commit
0eeb63bbde
6 files changed
+188
-7
No files matched your search
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -1,7 +1,10 @@
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
|
import Script from "next/script";
|
||||||
import { ThemeProvider } from "@/app/providers";
|
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
|
// Use Inter como fallback já que é uma fonte similar à Geist
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
@@ -79,7 +82,19 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="pt-BR" suppressHydrationWarning className={inter.className}>
|
<html lang="pt-BR" suppressHydrationWarning className={inter.className}>
|
||||||
<body className="min-h-screen bg-background antialiased">
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export default function ProdutoPage() {
|
|||||||
<div className="w-2 h-6 bg-blue-600 rounded-full" />
|
<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>
|
Composição Nutricional <span className="text-slate-400 font-mono text-xs font-normal">/ 100g</span>
|
||||||
</h3>
|
</h3>
|
||||||
<NutritionGrid nutritionData={formatNutritionData(product)} />
|
<NutritionGrid nutritionData={formatNutritionData(product) || {}} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,32 @@ const nextConfig = {
|
|||||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
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
|
// Headers de segurança e performance
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
|
|||||||
+5
-5
@@ -5,12 +5,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"postinstall": "./node_modules/.bin/prisma generate",
|
"postinstall": "prisma generate",
|
||||||
"build": "./node_modules/.bin/prisma generate && ./node_modules/.bin/prisma migrate deploy && next build",
|
"build": "prisma generate && prisma migrate deploy && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"db:push": "./node_modules/.bin/prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:generate": "./node_modules/.bin/prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:migrate": "./node_modules/.bin/prisma migrate deploy"
|
"db:migrate": "prisma migrate deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.22.0",
|
"@prisma/client": "^5.22.0",
|
||||||
|
|||||||
Reference in new issue
Block a user