mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
Implementando Rudderstack
This commit is contained in:
parent
1542572be4
commit
6e9d847c77
@ -23,13 +23,12 @@
|
|||||||
Referrer-Policy = "strict-origin-when-cross-origin"
|
Referrer-Policy = "strict-origin-when-cross-origin"
|
||||||
Content-Security-Policy = """
|
Content-Security-Policy = """
|
||||||
default-src 'self';
|
default-src 'self';
|
||||||
connect-src 'self' https://bsjlbnyslxzsdwxvkaap.supabase.co wss://bsjlbnyslxzsdwxvkaap.supabase.co *.sentry.io *.ingest.sentry.io;
|
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.rudderlabs.com https://www.googletagmanager.com;
|
||||||
img-src 'self' data: https: blob:;
|
connect-src 'self' https://*.rudderlabs.com https://*.ingest.sentry.io https://*.supabase.co https://www.google-analytics.com;
|
||||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://bsjlbnyslxzsdwxvkaap.supabase.co *.sentry-cdn.com;
|
|
||||||
style-src 'self' 'unsafe-inline';
|
style-src 'self' 'unsafe-inline';
|
||||||
frame-src 'self' https://bsjlbnyslxzsdwxvkaap.supabase.co;
|
img-src 'self' data: https:;
|
||||||
font-src 'self' data:;
|
font-src 'self' data:;
|
||||||
media-src 'self' https://bsjlbnyslxzsdwxvkaap.supabase.co;
|
frame-src 'self';
|
||||||
worker-src 'self' blob:;
|
worker-src 'self' blob:;
|
||||||
"""
|
"""
|
||||||
Access-Control-Allow-Origin = "https://historiasmagicas.netlify.app"
|
Access-Control-Allow-Origin = "https://historiasmagicas.netlify.app"
|
||||||
|
|||||||
20
src/components/analytics/PageTracker.tsx
Normal file
20
src/components/analytics/PageTracker.tsx
Normal 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;
|
||||||
|
}
|
||||||
62
src/components/analytics/RudderstackAnalytics.tsx
Normal file
62
src/components/analytics/RudderstackAnalytics.tsx
Normal 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;
|
||||||
|
}
|
||||||
@ -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 { LogIn, Eye, EyeOff, School, GraduationCap, User } from 'lucide-react';
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { useDataLayer } from '../../hooks/useDataLayer';
|
import { useDataLayer } from '../../hooks/useDataLayer';
|
||||||
|
import { useFormTracking } from '../../hooks/useFormTracking';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
userType: 'school' | 'teacher' | 'student';
|
userType: 'school' | 'teacher' | 'student';
|
||||||
@ -32,6 +34,15 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
|||||||
const { signIn } = useAuth();
|
const { signIn } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { trackEvent } = useDataLayer();
|
const { trackEvent } = useDataLayer();
|
||||||
|
const formTracking = useFormTracking({
|
||||||
|
formId: 'login-form',
|
||||||
|
formName: `${userType}-login`,
|
||||||
|
category: 'auth'
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formTracking.trackFormStarted();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -48,9 +59,13 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
|||||||
|
|
||||||
console.log('Resposta do Supabase:', { data, error });
|
console.log('Resposta do Supabase:', { data, error });
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) {
|
||||||
|
formTracking.trackFormError('auth_error', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
if (!data.user) {
|
if (!data.user) {
|
||||||
|
formTracking.trackFormError('user_not_found', 'Usuário não encontrado');
|
||||||
throw new Error('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);
|
console.log('Role atual:', userRole);
|
||||||
|
|
||||||
if (userRole !== userType) {
|
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]}`);
|
throw new Error(`Este não é um login de ${userTypeLabels[userType]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formTracking.trackFormSubmitted(true, {
|
||||||
|
user_type: userType,
|
||||||
|
user_id: data.user.id
|
||||||
|
});
|
||||||
|
|
||||||
switch (userType) {
|
switch (userType) {
|
||||||
case 'school':
|
case 'school':
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
@ -85,12 +106,30 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
|||||||
} else {
|
} else {
|
||||||
setError('Email ou senha incorretos');
|
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);
|
trackEvent('auth', 'login_error', err.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-b from-purple-50 to-white py-12">
|
<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">
|
<div className="max-w-md mx-auto px-4">
|
||||||
@ -123,7 +162,9 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
|||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
value={email}
|
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"
|
className="mt-1 block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -138,7 +179,9 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
|||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
required
|
required
|
||||||
value={password}
|
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"
|
className="block w-full rounded-lg border-gray-300 shadow-sm focus:border-purple-500 focus:ring-purple-500 pr-10"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@ -155,32 +198,49 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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 ? (
|
{loading ? (
|
||||||
'Entrando...'
|
'Entrando...'
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<LogIn className="h-5 w-5" />
|
<LogIn className="h-5 w-5 mr-2" />
|
||||||
Entrar
|
Entrar
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{onRegisterClick && (
|
{onRegisterClick && (
|
||||||
<div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Ainda não tem uma conta?{' '}
|
Ainda não tem uma conta?{' '}
|
||||||
<button
|
<Button
|
||||||
|
trackingId="register-link"
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
onClick={onRegisterClick}
|
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
|
Cadastre-se
|
||||||
</button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,27 +1,76 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useButtonTracking } from '../../hooks/useButtonTracking';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
as?: 'button' | 'span';
|
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({
|
export function Button({
|
||||||
as: Component = 'button',
|
as: Component = 'button',
|
||||||
className = '',
|
|
||||||
children,
|
children,
|
||||||
|
className = '',
|
||||||
|
trackingId,
|
||||||
|
variant = 'default',
|
||||||
|
size = 'md',
|
||||||
|
trackingProperties,
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
type = 'button',
|
||||||
...props
|
...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 (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={`
|
type={Component === 'button' ? type : undefined}
|
||||||
inline-flex items-center justify-center px-4 py-2
|
className={baseStyles}
|
||||||
text-sm font-medium text-white
|
onClick={handleClick}
|
||||||
bg-purple-600 hover:bg-purple-700
|
disabled={disabled}
|
||||||
rounded-md shadow-sm
|
|
||||||
transition-colors duration-200
|
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed
|
|
||||||
${className}
|
|
||||||
`}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
50
src/hooks/useButtonTracking.ts
Normal file
50
src/hooks/useButtonTracking.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
83
src/hooks/useFormTracking.ts
Normal file
83
src/hooks/useFormTracking.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
55
src/hooks/useRudderstack.ts
Normal file
55
src/hooks/useRudderstack.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -6,11 +6,16 @@ import * as Sentry from "@sentry/react";
|
|||||||
import { router } from './routes';
|
import { router } from './routes';
|
||||||
import { Toaster } from './components/ui/toaster';
|
import { Toaster } from './components/ui/toaster';
|
||||||
import { GoogleTagManager } from './components/analytics/GoogleTagManager';
|
import { GoogleTagManager } from './components/analytics/GoogleTagManager';
|
||||||
|
import { RudderstackAnalytics } from './components/analytics/RudderstackAnalytics';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
// GTM ID - Substitua pelo seu ID real do GTM
|
// GTM ID - Substitua pelo seu ID real do GTM
|
||||||
const GTM_ID = import.meta.env.VITE_GTM_ID;
|
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
|
// Inicialização do Sentry
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: "https://6c15876055bf4a860c1b63a8e4e7ca65@o544400.ingest.us.sentry.io/4508626073092096",
|
dsn: "https://6c15876055bf4a860c1b63a8e4e7ca65@o544400.ingest.us.sentry.io/4508626073092096",
|
||||||
@ -54,6 +59,10 @@ function Root() {
|
|||||||
return (
|
return (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<GoogleTagManager gtmId={GTM_ID} />
|
<GoogleTagManager gtmId={GTM_ID} />
|
||||||
|
<RudderstackAnalytics
|
||||||
|
writeKey={RUDDERSTACK_WRITE_KEY}
|
||||||
|
dataPlaneUrl={RUDDERSTACK_DATA_PLANE_URL}
|
||||||
|
/>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|||||||
@ -32,11 +32,21 @@ import { TestWordHighlighter } from './pages/TestWordHighlighter';
|
|||||||
import { ExercisePage } from './pages/student-dashboard/ExercisePage';
|
import { ExercisePage } from './pages/student-dashboard/ExercisePage';
|
||||||
import { EvidenceBased } from './pages/landing/EvidenceBased';
|
import { EvidenceBased } from './pages/landing/EvidenceBased';
|
||||||
import { TextSalesLetter } from './pages/landing/TextSalesLetter';
|
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([
|
export const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <HomePage />,
|
element: <RootLayout><HomePage /></RootLayout>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/teste',
|
path: '/teste',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user