Implementando Rudderstack

This commit is contained in:
Lucas Santana 2025-01-11 14:45:47 -03:00
parent 1542572be4
commit 6e9d847c77
10 changed files with 428 additions and 31 deletions

View File

@ -23,13 +23,12 @@
Referrer-Policy = "strict-origin-when-cross-origin"
Content-Security-Policy = """
default-src 'self';
connect-src 'self' https://bsjlbnyslxzsdwxvkaap.supabase.co wss://bsjlbnyslxzsdwxvkaap.supabase.co *.sentry.io *.ingest.sentry.io;
img-src 'self' data: https: blob:;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://bsjlbnyslxzsdwxvkaap.supabase.co *.sentry-cdn.com;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.rudderlabs.com https://www.googletagmanager.com;
connect-src 'self' https://*.rudderlabs.com https://*.ingest.sentry.io https://*.supabase.co https://www.google-analytics.com;
style-src 'self' 'unsafe-inline';
frame-src 'self' https://bsjlbnyslxzsdwxvkaap.supabase.co;
img-src 'self' data: https:;
font-src 'self' data:;
media-src 'self' https://bsjlbnyslxzsdwxvkaap.supabase.co;
frame-src 'self';
worker-src 'self' blob:;
"""
Access-Control-Allow-Origin = "https://historiasmagicas.netlify.app"

View File

@ -0,0 +1,20 @@
import { useEffect } from 'react';
import { useRudderstack } from '../../hooks/useRudderstack';
import { useLocation } from 'react-router-dom';
export function PageTracker() {
const location = useLocation();
const { track } = useRudderstack();
useEffect(() => {
track('page_viewed', {
path: location.pathname,
url: window.location.href,
search: location.search,
title: document.title,
referrer: document.referrer,
});
}, [location, track]);
return null;
}

View File

@ -0,0 +1,62 @@
import React from 'react';
interface RudderstackAnalyticsProps {
writeKey: string;
dataPlaneUrl: string;
}
declare global {
interface Window {
rudderanalytics: any;
RudderSnippetVersion: string;
rudderAnalyticsBuildType: string;
rudderAnalyticsMount: () => void;
rudderAnalyticsAddScript: (url: string, attr?: string, value?: string) => void;
}
}
export function RudderstackAnalytics({ writeKey, dataPlaneUrl }: RudderstackAnalyticsProps) {
React.useEffect(() => {
// Verifica se o Rudderstack já está carregado
if (window.rudderanalytics?.loaded) {
return;
}
const script = document.createElement('script');
script.innerHTML = `
!function(){"use strict";window.RudderSnippetVersion="3.0.32";var e="rudderanalytics";window[e]||(window[e]=[]);
var rudderanalytics=window[e];if(Array.isArray(rudderanalytics)){
if(rudderanalytics.loaded){
console.warn("RudderStack JavaScript SDK snippet included more than once.");
return;
}
rudderanalytics.loaded=true;
window.rudderAnalyticsBuildType="legacy";var sdkBaseUrl="https://cdn.rudderlabs.com/v3";var sdkName="rsa.min.js";
var scriptLoadingMode="async";
var r=["setDefaultInstanceKey","load","ready","page","track","identify","alias","group","reset","setAnonymousId","startSession","endSession","consent"];
for(var n=0;n<r.length;n++){var t=r[n];rudderanalytics[t]=function(r){return function(){var n;
Array.isArray(window[e])?rudderanalytics.push([r].concat(Array.prototype.slice.call(arguments))):null===(n=window[e][r])||void 0===n||n.apply(window[e],arguments)
}}(t)}try{
new Function('class Test{field=()=>{};test({prop=[]}={}){return prop?(prop?.property??[...prop]):import("");}}'),
window.rudderAnalyticsBuildType="modern"}catch(o){}var d=document.head||document.getElementsByTagName("head")[0];
var i=document.body||document.getElementsByTagName("body")[0];window.rudderAnalyticsAddScript=function(e,r,n){
var t=document.createElement("script");t.src=e,t.setAttribute("data-loader","RS_JS_SDK"),r&&n&&t.setAttribute(r,n),
"async"===scriptLoadingMode?t.async=true:"defer"===scriptLoadingMode&&(t.defer=true),
d?d.insertBefore(t,d.firstChild):i.insertBefore(t,i.firstChild)},window.rudderAnalyticsMount=function(){!function(){
if("undefined"==typeof globalThis){var e;var r=function getGlobal(){
return"undefined"!=typeof self?self:"undefined"!=typeof window?window:null}();r&&Object.defineProperty(r,"globalThis",{
value:r,configurable:true})}
}(),window.rudderAnalyticsAddScript("".concat(sdkBaseUrl,"/").concat(window.rudderAnalyticsBuildType,"/").concat(sdkName),"data-rsa-write-key","${writeKey}")
},
"undefined"==typeof Promise||"undefined"==typeof globalThis?window.rudderAnalyticsAddScript("https://polyfill-fastly.io/v3/polyfill.min.js?version=3.111.0&features=Symbol%2CPromise&callback=rudderAnalyticsMount"):window.rudderAnalyticsMount();
var loadOptions={};rudderanalytics.load("${writeKey}","${dataPlaneUrl}",loadOptions)}}}();
`;
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
}, [writeKey, dataPlaneUrl]);
return null;
}

View File

@ -1,9 +1,11 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { LogIn, Eye, EyeOff, School, GraduationCap, User } from 'lucide-react';
import { useAuth } from '../../hooks/useAuth';
import { useNavigate } from 'react-router-dom';
import { supabase } from '../../lib/supabase';
import { useDataLayer } from '../../hooks/useDataLayer';
import { useFormTracking } from '../../hooks/useFormTracking';
import { Button } from '../ui/button';
interface LoginFormProps {
userType: 'school' | 'teacher' | 'student';
@ -32,6 +34,15 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
const { signIn } = useAuth();
const navigate = useNavigate();
const { trackEvent } = useDataLayer();
const formTracking = useFormTracking({
formId: 'login-form',
formName: `${userType}-login`,
category: 'auth'
});
useEffect(() => {
formTracking.trackFormStarted();
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@ -48,9 +59,13 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
console.log('Resposta do Supabase:', { data, error });
if (error) throw error;
if (error) {
formTracking.trackFormError('auth_error', error.message);
throw error;
}
if (!data.user) {
formTracking.trackFormError('user_not_found', 'Usuário não encontrado');
throw new Error('Usuário não encontrado');
}
@ -60,9 +75,15 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
console.log('Role atual:', userRole);
if (userRole !== userType) {
formTracking.trackFormError('invalid_role', `Este não é um login de ${userTypeLabels[userType]}`);
throw new Error(`Este não é um login de ${userTypeLabels[userType]}`);
}
formTracking.trackFormSubmitted(true, {
user_type: userType,
user_id: data.user.id
});
switch (userType) {
case 'school':
navigate('/dashboard');
@ -85,12 +106,30 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
} else {
setError('Email ou senha incorretos');
}
formTracking.trackFormSubmitted(false, {
error_type: err instanceof Error ? 'validation_error' : 'unknown_error',
error_message: err instanceof Error ? err.message : 'Email ou senha incorretos'
});
trackEvent('auth', 'login_error', err.message);
} finally {
setLoading(false);
}
};
const handleFieldChange = (field: string, value: string) => {
formTracking.trackFieldInteraction(field, 'change');
if (field === 'email') setEmail(value);
if (field === 'password') setPassword(value);
};
const handleFieldFocus = (field: string) => {
formTracking.trackFieldInteraction(field, 'focus');
};
const handleFieldBlur = (field: string) => {
formTracking.trackFieldInteraction(field, 'blur');
};
return (
<div className="min-h-screen bg-gradient-to-b from-purple-50 to-white py-12">
<div className="max-w-md mx-auto px-4">
@ -123,7 +162,9 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(e) => handleFieldChange('email', e.target.value)}
onFocus={() => handleFieldFocus('email')}
onBlur={() => handleFieldBlur('email')}
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
/>
</div>
@ -138,7 +179,9 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
type={showPassword ? 'text' : 'password'}
required
value={password}
onChange={(e) => setPassword(e.target.value)}
onChange={(e) => handleFieldChange('password', e.target.value)}
onFocus={() => handleFieldFocus('password')}
onBlur={() => handleFieldBlur('password')}
className="block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500 pr-10"
/>
<button
@ -155,32 +198,49 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
</div>
</div>
<button
<Button
type="submit"
disabled={loading}
className="w-full flex justify-center items-center gap-2 py-3 px-4 border border-transparent rounded-lg shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50"
trackingId="login-submit"
variant="primary"
size="lg"
trackingProperties={{
category: 'auth',
action: 'login_attempt',
label: `${userType}_login`,
value: 1,
}}
className="w-full"
>
{loading ? (
'Entrando...'
) : (
<>
<LogIn className="h-5 w-5" />
<LogIn className="h-5 w-5 mr-2" />
Entrar
</>
)}
</button>
</Button>
</form>
{onRegisterClick && (
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Ainda não tem uma conta?{' '}
<button
<Button
trackingId="register-link"
variant="link"
size="sm"
onClick={onRegisterClick}
className="text-purple-600 hover:text-purple-500 font-medium"
trackingProperties={{
category: 'auth',
action: 'register_click',
label: userType,
}}
className="text-purple-600 hover:text-purple-500 font-medium p-0"
>
Cadastre-se
</button>
</Button>
</p>
</div>
)}

View File

@ -1,27 +1,76 @@
import React from 'react';
import { useButtonTracking } from '../../hooks/useButtonTracking';
import { cn } from '../../lib/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
as?: 'button' | 'span';
children: React.ReactNode;
trackingId: string;
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
size?: 'sm' | 'md' | 'lg';
trackingProperties?: {
label?: string;
value?: string | number;
action?: string;
category?: string;
position?: string;
[key: string]: any;
};
}
export function Button({
as: Component = 'button',
className = '',
children,
className = '',
trackingId,
variant = 'default',
size = 'md',
trackingProperties,
onClick,
disabled,
type = 'button',
...props
}: ButtonProps): JSX.Element {
}: ButtonProps) {
const { trackButtonClick } = useButtonTracking({
category: trackingProperties?.category || 'interaction'
});
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
// Rastreia o clique
trackButtonClick(trackingId, {
variant,
size,
...trackingProperties,
});
// Chama o onClick original se existir
onClick?.(event);
};
const baseStyles = cn(
'inline-flex items-center justify-center px-4 py-2',
'text-sm font-medium',
'rounded-md shadow-sm',
'transition-colors duration-200',
'disabled:opacity-50 disabled:cursor-not-allowed',
{
'text-white bg-purple-600 hover:bg-purple-700': variant === 'primary' || variant === 'default',
'text-gray-700 bg-white border border-gray-300 hover:bg-gray-50': variant === 'secondary',
'text-purple-600 bg-transparent hover:bg-purple-50': variant === 'ghost',
'text-purple-600 bg-transparent hover:underline': variant === 'link',
'text-purple-600 border border-purple-600 hover:bg-purple-50': variant === 'outline',
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
},
className
);
return (
<Component
className={`
inline-flex items-center justify-center px-4 py-2
text-sm font-medium text-white
bg-purple-600 hover:bg-purple-700
rounded-md shadow-sm
transition-colors duration-200
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
type={Component === 'button' ? type : undefined}
className={baseStyles}
onClick={handleClick}
disabled={disabled}
{...props}
>
{children}

View File

@ -0,0 +1,50 @@
import { useRudderstack } from './useRudderstack';
interface ButtonTrackingOptions {
category?: string;
location?: string;
}
export function useButtonTracking(options: ButtonTrackingOptions = {}) {
const { track } = useRudderstack();
const { category = 'interaction', location = window.location.pathname } = options;
const trackButtonClick = (
buttonId: string,
properties?: {
label?: string;
value?: string | number;
action?: string;
variant?: string;
position?: string;
[key: string]: any;
}
) => {
track('button_clicked', {
button_id: buttonId,
category,
location,
timestamp: new Date().toISOString(),
...properties,
// Informações da página
page_title: document.title,
page_url: window.location.href,
page_path: window.location.pathname,
// Informações do dispositivo/viewport
viewport_width: window.innerWidth,
viewport_height: window.innerHeight,
device_type: getDeviceType(),
});
};
const getDeviceType = () => {
const width = window.innerWidth;
if (width < 768) return 'mobile';
if (width < 1024) return 'tablet';
return 'desktop';
};
return {
trackButtonClick,
};
}

View File

@ -0,0 +1,83 @@
import { useRudderstack } from './useRudderstack';
interface FormTrackingOptions {
formId: string;
formName: string;
category?: string;
}
export function useFormTracking({ formId, formName, category = 'form' }: FormTrackingOptions) {
const { track } = useRudderstack();
const trackFormStarted = () => {
track('form_started', {
form_id: formId,
form_name: formName,
category,
});
};
const trackFormStepCompleted = (step: string, isValid: boolean) => {
track('form_step_completed', {
form_id: formId,
form_name: formName,
category,
step,
is_valid: isValid,
});
};
const trackFormSubmitted = (isValid: boolean, fields?: Record<string, any>) => {
track('form_submitted', {
form_id: formId,
form_name: formName,
category,
is_valid: isValid,
...fields,
});
};
const trackFormError = (errorType: string, errorMessage: string, field?: string) => {
track('form_error', {
form_id: formId,
form_name: formName,
category,
error_type: errorType,
error_message: errorMessage,
field,
});
};
const trackFormAbandoned = (step?: string) => {
track('form_abandoned', {
form_id: formId,
form_name: formName,
category,
step,
});
};
const trackFieldInteraction = (
fieldName: string,
interactionType: 'focus' | 'blur' | 'change',
value?: string
) => {
track('form_field_interaction', {
form_id: formId,
form_name: formName,
category,
field_name: fieldName,
interaction_type: interactionType,
field_value: value,
});
};
return {
trackFormStarted,
trackFormStepCompleted,
trackFormSubmitted,
trackFormError,
trackFormAbandoned,
trackFieldInteraction,
};
}

View File

@ -0,0 +1,55 @@
interface RudderstackEvent {
event: string;
properties?: Record<string, any>;
}
interface UserTraits {
[key: string]: any;
}
export function useRudderstack() {
const track = (eventName: string, properties?: Record<string, any>) => {
if (window.rudderanalytics) {
window.rudderanalytics.track(eventName, properties);
}
};
const page = (name?: string, properties?: Record<string, any>) => {
if (window.rudderanalytics) {
window.rudderanalytics.page(name, properties);
}
};
const identify = (userId: string, traits?: UserTraits) => {
if (window.rudderanalytics) {
window.rudderanalytics.identify(userId, traits);
}
};
const group = (groupId: string, traits?: Record<string, any>) => {
if (window.rudderanalytics) {
window.rudderanalytics.group(groupId, traits);
}
};
const alias = (newUserId: string) => {
if (window.rudderanalytics) {
window.rudderanalytics.alias(newUserId);
}
};
const reset = () => {
if (window.rudderanalytics) {
window.rudderanalytics.reset();
}
};
return {
track,
page,
identify,
group,
alias,
reset,
};
}

View File

@ -6,11 +6,16 @@ import * as Sentry from "@sentry/react";
import { router } from './routes';
import { Toaster } from './components/ui/toaster';
import { GoogleTagManager } from './components/analytics/GoogleTagManager';
import { RudderstackAnalytics } from './components/analytics/RudderstackAnalytics';
import './index.css';
// GTM ID - Substitua pelo seu ID real do GTM
const GTM_ID = import.meta.env.VITE_GTM_ID;
// Rudderstack Config
const RUDDERSTACK_WRITE_KEY = import.meta.env.VITE_RUDDERSTACK_WRITE_KEY;
const RUDDERSTACK_DATA_PLANE_URL = import.meta.env.VITE_RUDDERSTACK_DATA_PLANE_URL;
// Inicialização do Sentry
Sentry.init({
dsn: "https://6c15876055bf4a860c1b63a8e4e7ca65@o544400.ingest.us.sentry.io/4508626073092096",
@ -54,6 +59,10 @@ function Root() {
return (
<StrictMode>
<GoogleTagManager gtmId={GTM_ID} />
<RudderstackAnalytics
writeKey={RUDDERSTACK_WRITE_KEY}
dataPlaneUrl={RUDDERSTACK_DATA_PLANE_URL}
/>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<Toaster />

View File

@ -32,11 +32,21 @@ import { TestWordHighlighter } from './pages/TestWordHighlighter';
import { ExercisePage } from './pages/student-dashboard/ExercisePage';
import { EvidenceBased } from './pages/landing/EvidenceBased';
import { TextSalesLetter } from './pages/landing/TextSalesLetter';
import { PageTracker } from './components/analytics/PageTracker';
function RootLayout({ children }: { children: React.ReactNode }) {
return (
<>
<PageTracker />
{children}
</>
);
}
export const router = createBrowserRouter([
{
path: '/',
element: <HomePage />,
element: <RootLayout><HomePage /></RootLayout>,
},
{
path: '/teste',