mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 14:27:51 +00:00
feat: Melhorando tracking com o Rudderstack
Some checks are pending
Docker Build and Push / build (push) Waiting to run
Some checks are pending
Docker Build and Push / build (push) Waiting to run
This commit is contained in:
parent
09c4894a1c
commit
41a225d460
@ -1,83 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface RudderstackAnalyticsProps {
|
|
||||||
writeKey: string;
|
|
||||||
dataPlaneUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
rudderanalytics: any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RudderstackAnalytics({ writeKey, dataPlaneUrl }: RudderstackAnalyticsProps) {
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (window.rudderanalytics?.loaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa o objeto rudderanalytics
|
|
||||||
window.rudderanalytics = window.rudderanalytics || [];
|
|
||||||
|
|
||||||
// Define os métodos básicos
|
|
||||||
const methods = ['load', 'page', 'track', 'identify', 'alias', 'group', 'ready', 'reset'];
|
|
||||||
methods.forEach((method) => {
|
|
||||||
window.rudderanalytics[method] = function() {
|
|
||||||
window.rudderanalytics.push([method].concat(Array.prototype.slice.call(arguments)));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Carrega o script do Rudderstack
|
|
||||||
const loadScript = () => {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = 'https://cdn.rudderlabs.com/v1.1/rudder-analytics.min.js';
|
|
||||||
script.async = true;
|
|
||||||
script.crossOrigin = 'anonymous';
|
|
||||||
|
|
||||||
script.onload = () => {
|
|
||||||
window.rudderanalytics.load(writeKey, dataPlaneUrl, {
|
|
||||||
configUrl: 'https://api.rudderlabs.com',
|
|
||||||
destSDKBaseURL: 'https://cdn.rudderlabs.com/v1.1',
|
|
||||||
logLevel: 'ERROR',
|
|
||||||
secureCookie: true,
|
|
||||||
integrations: { All: true },
|
|
||||||
loadIntegration: true,
|
|
||||||
sendAdblockPage: true,
|
|
||||||
sendAdblockPageOptions: {
|
|
||||||
integrations: { All: true }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
script.onerror = (error) => {
|
|
||||||
console.error('Erro ao carregar Rudderstack:', error);
|
|
||||||
reject(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.head.appendChild(script);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Carrega o script e inicializa
|
|
||||||
loadScript()
|
|
||||||
.then(() => {
|
|
||||||
window.rudderanalytics.page();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Falha ao inicializar Rudderstack:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const script = document.querySelector('script[src*="rudder-analytics.min.js"]');
|
|
||||||
if (script && script.parentNode) {
|
|
||||||
script.parentNode.removeChild(script);
|
|
||||||
}
|
|
||||||
delete window.rudderanalytics;
|
|
||||||
};
|
|
||||||
}, [writeKey, dataPlaneUrl]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { processAudio } from '../../services/audioService';
|
import { processAudio } from '../../services/audioService';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
|
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||||
|
|
||||||
interface AudioUploaderProps {
|
interface AudioUploaderProps {
|
||||||
storyId: string;
|
storyId: string;
|
||||||
@ -59,7 +60,7 @@ export function AudioUploader({
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
trackingId="audio-upload-button"
|
trackingId="audio-upload-button"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'audio',
|
category: EVENT_CATEGORIES.AUDIO,
|
||||||
action: 'upload_click',
|
action: 'upload_click',
|
||||||
label: 'audio_uploader'
|
label: 'audio_uploader'
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useDataLayer } from '../../hooks/useDataLayer';
|
|||||||
import { useFormTracking } from '../../hooks/useFormTracking';
|
import { useFormTracking } from '../../hooks/useFormTracking';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { useErrorTracking } from '../../hooks/useErrorTracking';
|
import { useErrorTracking } from '../../hooks/useErrorTracking';
|
||||||
|
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||||
|
|
||||||
interface LoginFormProps {
|
interface LoginFormProps {
|
||||||
userType: 'school' | 'teacher' | 'student';
|
userType: 'school' | 'teacher' | 'student';
|
||||||
@ -224,7 +225,7 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
size="lg"
|
size="lg"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'auth',
|
category: EVENT_CATEGORIES.AUTH,
|
||||||
action: 'login_attempt',
|
action: 'login_attempt',
|
||||||
label: `${userType}_login`,
|
label: `${userType}_login`,
|
||||||
value: 1,
|
value: 1,
|
||||||
@ -252,7 +253,7 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={onRegisterClick}
|
onClick={onRegisterClick}
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'auth',
|
category: EVENT_CATEGORIES.AUTH,
|
||||||
action: 'register_click',
|
action: 'register_click',
|
||||||
label: userType,
|
label: userType,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { ProcessStep } from '@/components/ui/process-step';
|
|||||||
import { InfoCard } from '@/components/ui/info-card';
|
import { InfoCard } from '@/components/ui/info-card';
|
||||||
import { ComparisonSection } from '@/components/ui/comparison-section';
|
import { ComparisonSection } from '@/components/ui/comparison-section';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ name: 'Início', href: '/' },
|
{ name: 'Início', href: '/' },
|
||||||
@ -55,7 +56,7 @@ export function HomePage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
trackingId="nav_login_button"
|
trackingId="nav_login_button"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'navigation',
|
category: EVENT_CATEGORIES.NAVIGATION,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: 'login_dropdown'
|
label: 'login_dropdown'
|
||||||
}}
|
}}
|
||||||
@ -71,7 +72,7 @@ export function HomePage() {
|
|||||||
className="w-full text-left"
|
className="w-full text-left"
|
||||||
trackingId="nav_school_login"
|
trackingId="nav_school_login"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'navigation',
|
category: EVENT_CATEGORIES.NAVIGATION,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: 'school_login'
|
label: 'school_login'
|
||||||
}}
|
}}
|
||||||
@ -84,7 +85,7 @@ export function HomePage() {
|
|||||||
className="w-full text-left"
|
className="w-full text-left"
|
||||||
trackingId="nav_teacher_login"
|
trackingId="nav_teacher_login"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'navigation',
|
category: EVENT_CATEGORIES.NAVIGATION,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: 'teacher_login'
|
label: 'teacher_login'
|
||||||
}}
|
}}
|
||||||
@ -97,7 +98,7 @@ export function HomePage() {
|
|||||||
className="w-full text-left"
|
className="w-full text-left"
|
||||||
trackingId="nav_student_login"
|
trackingId="nav_student_login"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'navigation',
|
category: EVENT_CATEGORIES.NAVIGATION,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: 'student_login'
|
label: 'student_login'
|
||||||
}}
|
}}
|
||||||
@ -113,7 +114,7 @@ export function HomePage() {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
trackingId="nav_register_button"
|
trackingId="nav_register_button"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'navigation',
|
category: EVENT_CATEGORIES.NAVIGATION,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: 'register_school'
|
label: 'register_school'
|
||||||
}}
|
}}
|
||||||
@ -149,7 +150,7 @@ export function HomePage() {
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
trackingId="hero_register_button"
|
trackingId="hero_register_button"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'hero',
|
category: EVENT_CATEGORIES.HERO,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: 'start_free',
|
label: 'start_free',
|
||||||
position: 'hero_section'
|
position: 'hero_section'
|
||||||
@ -165,7 +166,7 @@ export function HomePage() {
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
trackingId="hero_demo_button"
|
trackingId="hero_demo_button"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'hero',
|
category: EVENT_CATEGORIES.HERO,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: 'watch_demo',
|
label: 'watch_demo',
|
||||||
position: 'hero_section'
|
position: 'hero_section'
|
||||||
@ -205,7 +206,7 @@ export function HomePage() {
|
|||||||
className="absolute inset-0 flex items-center justify-center bg-black/30 hover:bg-black/40 transition group"
|
className="absolute inset-0 flex items-center justify-center bg-black/30 hover:bg-black/40 transition group"
|
||||||
trackingId="hero_video_play"
|
trackingId="hero_video_play"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'hero',
|
category: EVENT_CATEGORIES.HERO,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: 'play_demo_video',
|
label: 'play_demo_video',
|
||||||
position: 'hero_video'
|
position: 'hero_video'
|
||||||
|
|||||||
@ -1,20 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useButtonTracking } from '../../hooks/useButtonTracking';
|
import { useButtonTracking } from '../../hooks/useButtonTracking';
|
||||||
|
import { ButtonTrackingOptions } from '../../types/analytics';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||||
|
|
||||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
as?: 'button' | 'span';
|
as?: 'button' | 'span';
|
||||||
trackingId: string;
|
trackingId: string;
|
||||||
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
|
variant?: 'default' | 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
trackingProperties?: {
|
trackingProperties?: ButtonTrackingOptions;
|
||||||
label?: string;
|
|
||||||
value?: string | number;
|
|
||||||
action?: string;
|
|
||||||
category?: string;
|
|
||||||
position?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Button({
|
export function Button({
|
||||||
@ -31,18 +26,18 @@ export function Button({
|
|||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const { trackButtonClick } = useButtonTracking({
|
const { trackButtonClick } = useButtonTracking({
|
||||||
category: trackingProperties?.category || 'interaction'
|
category: EVENT_CATEGORIES.INTERACTION,
|
||||||
|
element_type: 'button',
|
||||||
|
...trackingProperties
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
// Rastreia o clique
|
|
||||||
trackButtonClick(trackingId, {
|
trackButtonClick(trackingId, {
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
...trackingProperties,
|
...trackingProperties,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Chama o onClick original se existir
|
|
||||||
onClick?.(event);
|
onClick?.(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
47
src/components/ui/form.tsx
Normal file
47
src/components/ui/form.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useFormTracking } from '../../hooks/useFormTracking';
|
||||||
|
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||||
|
|
||||||
|
interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
|
||||||
|
formId: string;
|
||||||
|
formName?: string;
|
||||||
|
trackingProperties?: {
|
||||||
|
category?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Form({
|
||||||
|
formId,
|
||||||
|
formName = formId,
|
||||||
|
children,
|
||||||
|
trackingProperties,
|
||||||
|
onSubmit,
|
||||||
|
...props
|
||||||
|
}: FormProps) {
|
||||||
|
const { trackFormStarted, trackFormSubmitted } = useFormTracking({
|
||||||
|
formId,
|
||||||
|
formName,
|
||||||
|
category: EVENT_CATEGORIES.FORM,
|
||||||
|
...trackingProperties
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
trackFormSubmitted(true);
|
||||||
|
onSubmit?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
trackFormStarted();
|
||||||
|
}, [formId, trackFormStarted]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
{...props}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/components/ui/link.tsx
Normal file
41
src/components/ui/link.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { useButtonTracking } from '../../hooks/useButtonTracking';
|
||||||
|
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||||
|
import { ButtonTrackingOptions } from '../../types/analytics';
|
||||||
|
|
||||||
|
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||||
|
to: string;
|
||||||
|
trackingId?: string;
|
||||||
|
trackingProperties?: ButtonTrackingOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Link({
|
||||||
|
to,
|
||||||
|
children,
|
||||||
|
trackingId,
|
||||||
|
trackingProperties,
|
||||||
|
...props
|
||||||
|
}: LinkProps) {
|
||||||
|
const { trackButtonClick } = useButtonTracking({
|
||||||
|
category: EVENT_CATEGORIES.INTERACTION,
|
||||||
|
element_type: 'link',
|
||||||
|
...trackingProperties
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
if (trackingId) {
|
||||||
|
trackButtonClick(trackingId);
|
||||||
|
}
|
||||||
|
props.onClick?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouterLink
|
||||||
|
to={to}
|
||||||
|
{...props}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RouterLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CheckCircle } from 'lucide-react';
|
import { CheckCircle } from 'lucide-react';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
|
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||||
|
|
||||||
interface PlanProps {
|
interface PlanProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -120,7 +121,7 @@ export function PlanForParents({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
trackingId={`parent_plan_subscribe_${plan.title.toLowerCase().replace(/\s+/g, '_')}`}
|
trackingId={`parent_plan_subscribe_${plan.title.toLowerCase().replace(/\s+/g, '_')}`}
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'pricing',
|
category: EVENT_CATEGORIES.PRICING,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: plan.title,
|
label: plan.title,
|
||||||
value: parseFloat(plan.price.replace(/[.,]/g, '')),
|
value: parseFloat(plan.price.replace(/[.,]/g, '')),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CheckCircle } from 'lucide-react';
|
import { CheckCircle } from 'lucide-react';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
|
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||||
|
|
||||||
interface PlanProps {
|
interface PlanProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -109,7 +110,7 @@ export function PlanForSchools({
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
trackingId={`plan_subscribe_${plan.title.toLowerCase().replace(/\s+/g, '_')}`}
|
trackingId={`plan_subscribe_${plan.title.toLowerCase().replace(/\s+/g, '_')}`}
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'pricing',
|
category: EVENT_CATEGORIES.PRICING,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: plan.title,
|
label: plan.title,
|
||||||
value: parseFloat(plan.price.replace(/[.,]/g, '')),
|
value: parseFloat(plan.price.replace(/[.,]/g, '')),
|
||||||
@ -138,7 +139,7 @@ export function PlanForSchools({
|
|||||||
className="gap-2"
|
className="gap-2"
|
||||||
trackingId="pricing_contact_button"
|
trackingId="pricing_contact_button"
|
||||||
trackingProperties={{
|
trackingProperties={{
|
||||||
category: 'pricing',
|
category: EVENT_CATEGORIES.PRICING,
|
||||||
action: 'click',
|
action: 'click',
|
||||||
label: 'contact_sales',
|
label: 'contact_sales',
|
||||||
position: 'footer',
|
position: 'footer',
|
||||||
|
|||||||
188
src/constants/analytics.ts
Normal file
188
src/constants/analytics.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
// Eventos de Página
|
||||||
|
export const PAGE_EVENTS = {
|
||||||
|
VIEW: 'page_viewed',
|
||||||
|
SCROLL: 'page_scrolled',
|
||||||
|
EXIT: 'page_exited',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Eventos de Usuário
|
||||||
|
export const USER_EVENTS = {
|
||||||
|
SIGN_UP: 'user_signed_up',
|
||||||
|
SIGN_IN: 'user_signed_in',
|
||||||
|
SIGN_OUT: 'user_signed_out',
|
||||||
|
UPDATE_PROFILE: 'user_updated_profile',
|
||||||
|
DELETE_ACCOUNT: 'user_deleted_account',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Eventos de Histórias
|
||||||
|
export const STORY_EVENTS = {
|
||||||
|
GENERATE: 'story_generated',
|
||||||
|
VIEW: 'story_viewed',
|
||||||
|
SHARE: 'story_shared',
|
||||||
|
LIKE: 'story_liked',
|
||||||
|
SAVE: 'story_saved',
|
||||||
|
PRINT: 'story_printed',
|
||||||
|
AUDIO_RECORD: 'story_audio_recorded',
|
||||||
|
AUDIO_PLAY: 'story_audio_played',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Eventos de Exercícios
|
||||||
|
export const EXERCISE_EVENTS = {
|
||||||
|
START: 'exercise_started',
|
||||||
|
COMPLETE: 'exercise_completed',
|
||||||
|
SKIP: 'exercise_skipped',
|
||||||
|
RETRY: 'exercise_retried',
|
||||||
|
SUBMIT_ANSWER: 'exercise_answer_submitted',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Eventos de Interação
|
||||||
|
export const INTERACTION_EVENTS = {
|
||||||
|
BUTTON_CLICK: 'button_clicked',
|
||||||
|
LINK_CLICK: 'link_clicked',
|
||||||
|
FORM_START: 'form_started',
|
||||||
|
FORM_SUBMIT: 'form_submitted',
|
||||||
|
FORM_ERROR: 'form_error_occurred',
|
||||||
|
MODAL_OPEN: 'modal_opened',
|
||||||
|
MODAL_CLOSE: 'modal_closed',
|
||||||
|
MENU_OPEN: 'menu_opened',
|
||||||
|
MENU_CLOSE: 'menu_closed',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Eventos de Erro
|
||||||
|
export const ERROR_EVENTS = {
|
||||||
|
API: 'api_error_occurred',
|
||||||
|
CLIENT: 'client_error_occurred',
|
||||||
|
VALIDATION: 'validation_error_occurred',
|
||||||
|
BOUNDARY: 'error_boundary_triggered',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Eventos de Assinatura
|
||||||
|
export const SUBSCRIPTION_EVENTS = {
|
||||||
|
VIEW_PLANS: 'subscription_plans_viewed',
|
||||||
|
SELECT_PLAN: 'subscription_plan_selected',
|
||||||
|
START_CHECKOUT: 'subscription_checkout_started',
|
||||||
|
COMPLETE_CHECKOUT: 'subscription_checkout_completed',
|
||||||
|
CANCEL: 'subscription_cancelled',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Categorias de Eventos
|
||||||
|
export const EVENT_CATEGORIES = {
|
||||||
|
PAGE: 'page',
|
||||||
|
USER: 'user',
|
||||||
|
STORY: 'story',
|
||||||
|
EXERCISE: 'exercise',
|
||||||
|
INTERACTION: 'interaction',
|
||||||
|
ERROR: 'error',
|
||||||
|
SUBSCRIPTION: 'subscription',
|
||||||
|
AUTH: 'auth',
|
||||||
|
NAVIGATION: 'navigation',
|
||||||
|
HERO: 'hero',
|
||||||
|
PRICING: 'pricing',
|
||||||
|
AUDIO: 'audio',
|
||||||
|
FORM: 'form'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Propriedades Comuns
|
||||||
|
export const EVENT_PROPERTIES = {
|
||||||
|
// Propriedades de Página
|
||||||
|
PAGE: {
|
||||||
|
URL: 'page_url',
|
||||||
|
TITLE: 'page_title',
|
||||||
|
REFERRER: 'page_referrer',
|
||||||
|
PATH: 'page_path',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Propriedades de Usuário
|
||||||
|
USER: {
|
||||||
|
ID: 'user_id',
|
||||||
|
TYPE: 'user_type',
|
||||||
|
ROLE: 'user_role',
|
||||||
|
SCHOOL: 'school_id',
|
||||||
|
CLASS: 'class_id',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Propriedades de Interação
|
||||||
|
INTERACTION: {
|
||||||
|
TYPE: 'interaction_type',
|
||||||
|
TARGET: 'interaction_target',
|
||||||
|
LOCATION: 'interaction_location',
|
||||||
|
VALUE: 'interaction_value',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Propriedades de Elemento
|
||||||
|
ELEMENT: {
|
||||||
|
ID: 'element_id',
|
||||||
|
TYPE: 'element_type',
|
||||||
|
NAME: 'element_name',
|
||||||
|
VALUE: 'element_value',
|
||||||
|
TEXT: 'element_text',
|
||||||
|
VARIANT: 'element_variant',
|
||||||
|
SIZE: 'element_size',
|
||||||
|
POSITION: 'element_position',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Propriedades de Erro
|
||||||
|
ERROR: {
|
||||||
|
TYPE: 'error_type',
|
||||||
|
MESSAGE: 'error_message',
|
||||||
|
CODE: 'error_code',
|
||||||
|
STACK: 'error_stack',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Propriedades de Tempo
|
||||||
|
TIMING: {
|
||||||
|
STARTED_AT: 'started_at',
|
||||||
|
COMPLETED_AT: 'completed_at',
|
||||||
|
DURATION: 'duration',
|
||||||
|
TIMESTAMP: 'timestamp',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Propriedades de Formulário
|
||||||
|
FORM: {
|
||||||
|
ID: 'form_id',
|
||||||
|
NAME: 'form_name',
|
||||||
|
TYPE: 'form_type',
|
||||||
|
STEP: 'form_step',
|
||||||
|
STATUS: 'form_status',
|
||||||
|
FIELDS: 'form_fields',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Valores de Propriedades
|
||||||
|
export const PROPERTY_VALUES = {
|
||||||
|
USER_TYPES: {
|
||||||
|
STUDENT: 'student',
|
||||||
|
TEACHER: 'teacher',
|
||||||
|
ADMIN: 'admin',
|
||||||
|
PARENT: 'parent',
|
||||||
|
},
|
||||||
|
|
||||||
|
INTERACTION_TYPES: {
|
||||||
|
CLICK: 'click',
|
||||||
|
HOVER: 'hover',
|
||||||
|
SCROLL: 'scroll',
|
||||||
|
SUBMIT: 'submit',
|
||||||
|
INPUT: 'input',
|
||||||
|
},
|
||||||
|
|
||||||
|
ERROR_TYPES: {
|
||||||
|
API: 'api',
|
||||||
|
CLIENT: 'client',
|
||||||
|
VALIDATION: 'validation',
|
||||||
|
BOUNDARY: 'boundary',
|
||||||
|
},
|
||||||
|
|
||||||
|
ELEMENT_SIZES: {
|
||||||
|
SMALL: 'sm',
|
||||||
|
MEDIUM: 'md',
|
||||||
|
LARGE: 'lg',
|
||||||
|
},
|
||||||
|
|
||||||
|
ELEMENT_VARIANTS: {
|
||||||
|
PRIMARY: 'primary',
|
||||||
|
SECONDARY: 'secondary',
|
||||||
|
OUTLINE: 'outline',
|
||||||
|
GHOST: 'ghost',
|
||||||
|
LINK: 'link',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
@ -1,26 +1,38 @@
|
|||||||
import { useRudderstack } from './useRudderstack';
|
import { analytics } from '../lib/analytics';
|
||||||
|
|
||||||
interface ButtonTrackingOptions {
|
interface ButtonTrackingOptions {
|
||||||
category?: string;
|
category?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useButtonTracking(options: ButtonTrackingOptions = {}) {
|
interface ButtonTrackingProperties {
|
||||||
const { track } = useRudderstack();
|
|
||||||
const { category = 'interaction', location = window.location.pathname } = options;
|
|
||||||
|
|
||||||
const trackButtonClick = (
|
|
||||||
buttonId: string,
|
|
||||||
properties?: {
|
|
||||||
label?: string;
|
label?: string;
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
action?: string;
|
action?: string;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
position?: string;
|
position?: string;
|
||||||
[key: string]: any;
|
// Propriedades específicas de tracking
|
||||||
}
|
flow?: string;
|
||||||
|
section?: string;
|
||||||
|
target?: string;
|
||||||
|
source?: string;
|
||||||
|
element_type?: string;
|
||||||
|
element_id?: string;
|
||||||
|
element_name?: string;
|
||||||
|
element_value?: string | number;
|
||||||
|
element_text?: string;
|
||||||
|
element_class?: string;
|
||||||
|
element_attributes?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useButtonTracking(options: ButtonTrackingOptions = {}) {
|
||||||
|
const { category = 'interaction', location = window.location.pathname } = options;
|
||||||
|
|
||||||
|
const trackButtonClick = (
|
||||||
|
buttonId: string,
|
||||||
|
properties?: ButtonTrackingProperties
|
||||||
) => {
|
) => {
|
||||||
track('button_clicked', {
|
analytics.track('button_clicked', {
|
||||||
button_id: buttonId,
|
button_id: buttonId,
|
||||||
category,
|
category,
|
||||||
location,
|
location,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { useRudderstack } from './useRudderstack';
|
import { analytics } from '../lib/analytics';
|
||||||
|
|
||||||
interface ErrorTrackingOptions {
|
interface ErrorTrackingOptions {
|
||||||
category?: string;
|
category?: string;
|
||||||
@ -8,7 +8,6 @@ interface ErrorTrackingOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
||||||
const { track } = useRudderstack();
|
|
||||||
const { category = 'error', userId, userEmail } = options;
|
const { category = 'error', userId, userEmail } = options;
|
||||||
|
|
||||||
const trackError = (
|
const trackError = (
|
||||||
@ -34,7 +33,7 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Rastreia no Rudderstack (para analytics)
|
// 2. Rastreia no Rudderstack (para analytics)
|
||||||
track('error_occurred', {
|
analytics.track('error_occurred', {
|
||||||
category,
|
category,
|
||||||
error_name: error.name,
|
error_name: error.name,
|
||||||
error_message: error.message,
|
error_message: error.message,
|
||||||
@ -64,7 +63,7 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Rastreia no Rudderstack
|
// 2. Rastreia no Rudderstack
|
||||||
track('error_boundary_triggered', {
|
analytics.track('error_boundary_triggered', {
|
||||||
category,
|
category,
|
||||||
error_name: error.name,
|
error_name: error.name,
|
||||||
error_message: error.message,
|
error_message: error.message,
|
||||||
@ -93,7 +92,7 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Rastreia no Rudderstack
|
// 2. Rastreia no Rudderstack
|
||||||
track('api_error_occurred', {
|
analytics.track('api_error_occurred', {
|
||||||
category,
|
category,
|
||||||
endpoint,
|
endpoint,
|
||||||
method,
|
method,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useRudderstack } from './useRudderstack';
|
import { analytics } from '../lib/analytics';
|
||||||
|
|
||||||
interface FormTrackingOptions {
|
interface FormTrackingOptions {
|
||||||
formId: string;
|
formId: string;
|
||||||
@ -7,10 +7,8 @@ interface FormTrackingOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useFormTracking({ formId, formName, category = 'form' }: FormTrackingOptions) {
|
export function useFormTracking({ formId, formName, category = 'form' }: FormTrackingOptions) {
|
||||||
const { track } = useRudderstack();
|
|
||||||
|
|
||||||
const trackFormStarted = () => {
|
const trackFormStarted = () => {
|
||||||
track('form_started', {
|
analytics.track('form_started', {
|
||||||
form_id: formId,
|
form_id: formId,
|
||||||
form_name: formName,
|
form_name: formName,
|
||||||
category,
|
category,
|
||||||
@ -18,7 +16,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
|||||||
};
|
};
|
||||||
|
|
||||||
const trackFormStepCompleted = (step: string, isValid: boolean) => {
|
const trackFormStepCompleted = (step: string, isValid: boolean) => {
|
||||||
track('form_step_completed', {
|
analytics.track('form_step_completed', {
|
||||||
form_id: formId,
|
form_id: formId,
|
||||||
form_name: formName,
|
form_name: formName,
|
||||||
category,
|
category,
|
||||||
@ -28,7 +26,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
|||||||
};
|
};
|
||||||
|
|
||||||
const trackFormSubmitted = (isValid: boolean, fields?: Record<string, any>) => {
|
const trackFormSubmitted = (isValid: boolean, fields?: Record<string, any>) => {
|
||||||
track('form_submitted', {
|
analytics.track('form_submitted', {
|
||||||
form_id: formId,
|
form_id: formId,
|
||||||
form_name: formName,
|
form_name: formName,
|
||||||
category,
|
category,
|
||||||
@ -38,7 +36,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
|||||||
};
|
};
|
||||||
|
|
||||||
const trackFormError = (errorType: string, errorMessage: string, field?: string) => {
|
const trackFormError = (errorType: string, errorMessage: string, field?: string) => {
|
||||||
track('form_error', {
|
analytics.track('form_error', {
|
||||||
form_id: formId,
|
form_id: formId,
|
||||||
form_name: formName,
|
form_name: formName,
|
||||||
category,
|
category,
|
||||||
@ -49,7 +47,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
|||||||
};
|
};
|
||||||
|
|
||||||
const trackFormAbandoned = (step?: string) => {
|
const trackFormAbandoned = (step?: string) => {
|
||||||
track('form_abandoned', {
|
analytics.track('form_abandoned', {
|
||||||
form_id: formId,
|
form_id: formId,
|
||||||
form_name: formName,
|
form_name: formName,
|
||||||
category,
|
category,
|
||||||
@ -62,7 +60,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
|||||||
interactionType: 'focus' | 'blur' | 'change',
|
interactionType: 'focus' | 'blur' | 'change',
|
||||||
value?: string
|
value?: string
|
||||||
) => {
|
) => {
|
||||||
track('form_field_interaction', {
|
analytics.track('form_field_interaction', {
|
||||||
form_id: formId,
|
form_id: formId,
|
||||||
form_name: formName,
|
form_name: formName,
|
||||||
category,
|
category,
|
||||||
|
|||||||
@ -1,47 +1,25 @@
|
|||||||
interface RudderstackEvent {
|
import { analytics } from '../lib/analytics';
|
||||||
event: string;
|
import { UserTraits } from '../types/analytics';
|
||||||
properties?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserTraits {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRudderstack() {
|
export function useRudderstack() {
|
||||||
const track = (eventName: string, properties?: Record<string, any>) => {
|
const track = (eventName: string, properties?: Record<string, unknown>) => {
|
||||||
if (window.rudderanalytics) {
|
analytics.track(eventName, properties);
|
||||||
window.rudderanalytics.track(eventName, properties);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = (name?: string, properties?: Record<string, any>) => {
|
const page = (name?: string, properties?: Record<string, unknown>) => {
|
||||||
if (window.rudderanalytics) {
|
analytics.page(name, properties);
|
||||||
window.rudderanalytics.page(name, properties);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const identify = (userId: string, traits?: UserTraits) => {
|
const identify = (userId: string, traits?: UserTraits) => {
|
||||||
if (window.rudderanalytics) {
|
analytics.identify({ id: userId, user_metadata: traits } as any);
|
||||||
window.rudderanalytics.identify(userId, traits);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const group = (groupId: string, traits?: Record<string, any>) => {
|
const group = (groupId: string, traits?: Record<string, unknown>) => {
|
||||||
if (window.rudderanalytics) {
|
analytics.group(groupId, traits);
|
||||||
window.rudderanalytics.group(groupId, traits);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const alias = (newUserId: string) => {
|
|
||||||
if (window.rudderanalytics) {
|
|
||||||
window.rudderanalytics.alias(newUserId);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
if (window.rudderanalytics) {
|
analytics.reset();
|
||||||
window.rudderanalytics.reset();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -49,7 +27,6 @@ export function useRudderstack() {
|
|||||||
page,
|
page,
|
||||||
identify,
|
identify,
|
||||||
group,
|
group,
|
||||||
alias,
|
|
||||||
reset,
|
reset,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { useRudderstack } from './useRudderstack';
|
import { analytics } from '../lib/analytics';
|
||||||
|
|
||||||
interface StoryGeneratedProps {
|
interface StoryGeneratedProps {
|
||||||
story_id: string;
|
story_id: string;
|
||||||
@ -47,38 +47,36 @@ interface InterestActionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useStudentTracking() {
|
export function useStudentTracking() {
|
||||||
const { track } = useRudderstack();
|
|
||||||
|
|
||||||
const trackStoryGenerated = (properties: StoryGeneratedProps) => {
|
const trackStoryGenerated = (properties: StoryGeneratedProps) => {
|
||||||
track('story_generated', {
|
analytics.track('story_generated', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackAudioRecorded = (properties: AudioRecordedProps) => {
|
const trackAudioRecorded = (properties: AudioRecordedProps) => {
|
||||||
track('audio_recorded', {
|
analytics.track('audio_recorded', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackExerciseCompleted = (properties: ExerciseCompletedProps) => {
|
const trackExerciseCompleted = (properties: ExerciseCompletedProps) => {
|
||||||
track('exercise_completed', {
|
analytics.track('exercise_completed', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackInterestAdded = (properties: InterestActionProps) => {
|
const trackInterestAdded = (properties: InterestActionProps) => {
|
||||||
track('interest_added', {
|
analytics.track('interest_added', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const trackInterestRemoved = (properties: InterestActionProps) => {
|
const trackInterestRemoved = (properties: InterestActionProps) => {
|
||||||
track('interest_removed', {
|
analytics.track('interest_removed', {
|
||||||
...properties,
|
...properties,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|||||||
252
src/lib/analytics/index.ts
Normal file
252
src/lib/analytics/index.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import { User } from '@supabase/supabase-js';
|
||||||
|
import * as Sentry from '@sentry/react';
|
||||||
|
import { EVENT_PROPERTIES } from '../../constants/analytics';
|
||||||
|
import { BaseEventProperties, UserTraits } from '../../types/analytics';
|
||||||
|
|
||||||
|
type RudderEvent = {
|
||||||
|
type: 'identify' | 'track' | 'page' | 'group' | 'reset';
|
||||||
|
args: unknown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RudderAnalytics {
|
||||||
|
identify: (userId: string, traits?: Record<string, unknown>) => void;
|
||||||
|
track: (eventName: string, properties?: Record<string, unknown>) => void;
|
||||||
|
page: (name?: string, properties?: Record<string, unknown>) => void;
|
||||||
|
group: (groupId: string, traits?: Record<string, unknown>) => void;
|
||||||
|
reset: () => void;
|
||||||
|
load: (writeKey: string, dataPlaneUrl: string, options?: Record<string, unknown>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
rudderanalytics: RudderAnalytics | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnalyticsOptions {
|
||||||
|
debug?: boolean;
|
||||||
|
writeKey?: string;
|
||||||
|
dataPlaneUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyticsError = Error | { message: string } | unknown;
|
||||||
|
|
||||||
|
export class Analytics {
|
||||||
|
private debug: boolean;
|
||||||
|
private user: User | null = null;
|
||||||
|
private writeKey?: string;
|
||||||
|
private dataPlaneUrl?: string;
|
||||||
|
private initialized = false;
|
||||||
|
private eventQueue: RudderEvent[] = [];
|
||||||
|
private initPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
constructor(options: AnalyticsOptions = {}) {
|
||||||
|
this.debug = options.debug || false;
|
||||||
|
this.writeKey = options.writeKey;
|
||||||
|
this.dataPlaneUrl = options.dataPlaneUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa o Rudderstack
|
||||||
|
init(options?: { writeKey?: string; dataPlaneUrl?: string }): Promise<void> {
|
||||||
|
if (this.initPromise) return this.initPromise;
|
||||||
|
|
||||||
|
this.initPromise = new Promise((resolve, reject) => {
|
||||||
|
const writeKey = options?.writeKey || this.writeKey || import.meta.env.VITE_RUDDERSTACK_WRITE_KEY;
|
||||||
|
const dataPlaneUrl = options?.dataPlaneUrl || this.dataPlaneUrl || import.meta.env.VITE_RUDDERSTACK_DATA_PLANE_URL;
|
||||||
|
|
||||||
|
if (!writeKey || !dataPlaneUrl) {
|
||||||
|
const error = new Error('Missing Rudderstack configuration');
|
||||||
|
this.handleError('init', error);
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.rudderlabs.com/v1.1/rudder-analytics.min.js';
|
||||||
|
script.async = true;
|
||||||
|
|
||||||
|
script.onload = () => {
|
||||||
|
if (window.rudderanalytics) {
|
||||||
|
window.rudderanalytics.load(writeKey, dataPlaneUrl, {
|
||||||
|
configUrl: 'https://api.rudderlabs.com',
|
||||||
|
logLevel: this.debug ? 'DEBUG' : 'ERROR',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Espera um pequeno intervalo para garantir que o Rudderstack está pronto
|
||||||
|
setTimeout(() => {
|
||||||
|
this.initialized = true;
|
||||||
|
this.processEventQueue();
|
||||||
|
if (this.debug) {
|
||||||
|
console.log('Rudderstack initialized successfully');
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
const error = new Error('Failed to load Rudderstack');
|
||||||
|
this.handleError('init', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
script.onerror = (error) => {
|
||||||
|
this.handleError('init', error);
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.head.appendChild(script);
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError('init', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.initPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private processEventQueue() {
|
||||||
|
while (this.eventQueue.length > 0) {
|
||||||
|
const event = this.eventQueue.shift();
|
||||||
|
if (!event) continue;
|
||||||
|
|
||||||
|
const analytics = window.rudderanalytics;
|
||||||
|
if (!analytics) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'identify':
|
||||||
|
analytics.identify(...(event.args as [string, Record<string, unknown>]));
|
||||||
|
break;
|
||||||
|
case 'track':
|
||||||
|
analytics.track(...(event.args as [string, Record<string, unknown>]));
|
||||||
|
break;
|
||||||
|
case 'page':
|
||||||
|
analytics.page(...(event.args as [string, Record<string, unknown>]));
|
||||||
|
break;
|
||||||
|
case 'group':
|
||||||
|
analytics.group(...(event.args as [string, Record<string, unknown>]));
|
||||||
|
break;
|
||||||
|
case 'reset':
|
||||||
|
analytics.reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(event.type, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueEvent(type: RudderEvent['type'], args: unknown[]) {
|
||||||
|
if (this.initialized && window.rudderanalytics) {
|
||||||
|
const analytics = window.rudderanalytics;
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'identify':
|
||||||
|
analytics.identify(...(args as [string, Record<string, unknown>]));
|
||||||
|
break;
|
||||||
|
case 'track':
|
||||||
|
analytics.track(...(args as [string, Record<string, unknown>]));
|
||||||
|
break;
|
||||||
|
case 'page':
|
||||||
|
analytics.page(...(args as [string, Record<string, unknown>]));
|
||||||
|
break;
|
||||||
|
case 'group':
|
||||||
|
analytics.group(...(args as [string, Record<string, unknown>]));
|
||||||
|
break;
|
||||||
|
case 'reset':
|
||||||
|
analytics.reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(type, error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.eventQueue.push({ type, args });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método para identificar usuário
|
||||||
|
identify(user: User | null) {
|
||||||
|
this.user = user;
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
const traits: UserTraits = {
|
||||||
|
email: user.email,
|
||||||
|
name: user.user_metadata?.name,
|
||||||
|
role: user.user_metadata?.role,
|
||||||
|
school_id: user.user_metadata?.school_id,
|
||||||
|
class_id: user.user_metadata?.class_id,
|
||||||
|
created_at: user.created_at,
|
||||||
|
updated_at: user.updated_at
|
||||||
|
};
|
||||||
|
|
||||||
|
this.queueEvent('identify', [user.id, traits]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método principal de tracking
|
||||||
|
track(eventName: string, properties?: Record<string, unknown>) {
|
||||||
|
const enrichedProperties = this.enrichEventProperties(properties);
|
||||||
|
this.queueEvent('track', [eventName, enrichedProperties]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método para tracking de página
|
||||||
|
page(name?: string, properties?: Record<string, unknown>) {
|
||||||
|
const enrichedProperties = this.enrichEventProperties(properties);
|
||||||
|
this.queueEvent('page', [name, enrichedProperties]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método para agrupar por escola
|
||||||
|
group(schoolId: string, properties?: Record<string, unknown>) {
|
||||||
|
const enrichedProperties = this.enrichEventProperties(properties);
|
||||||
|
this.queueEvent('group', [schoolId, enrichedProperties]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset do usuário
|
||||||
|
reset() {
|
||||||
|
this.user = null;
|
||||||
|
this.queueEvent('reset', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enriquece as propriedades com dados padrão
|
||||||
|
private enrichEventProperties(properties?: Record<string, unknown>): BaseEventProperties {
|
||||||
|
const baseProperties: BaseEventProperties = {
|
||||||
|
[EVENT_PROPERTIES.TIMING.TIMESTAMP]: new Date().toISOString(),
|
||||||
|
[EVENT_PROPERTIES.PAGE.URL]: window.location.href,
|
||||||
|
[EVENT_PROPERTIES.PAGE.TITLE]: document.title,
|
||||||
|
[EVENT_PROPERTIES.PAGE.PATH]: window.location.pathname,
|
||||||
|
environment: import.meta.env.MODE,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adiciona informações do usuário se disponível
|
||||||
|
if (this.user) {
|
||||||
|
baseProperties[EVENT_PROPERTIES.USER.ID] = this.user.id;
|
||||||
|
baseProperties[EVENT_PROPERTIES.USER.ROLE] = this.user.user_metadata?.role;
|
||||||
|
baseProperties[EVENT_PROPERTIES.USER.SCHOOL] = this.user.user_metadata?.school_id;
|
||||||
|
baseProperties[EVENT_PROPERTIES.USER.CLASS] = this.user.user_metadata?.class_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseProperties,
|
||||||
|
...properties
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tratamento de erros
|
||||||
|
private handleError(method: string, error: AnalyticsError) {
|
||||||
|
if (this.debug) {
|
||||||
|
console.error(`Analytics Error (${method}):`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Captura o erro no Sentry
|
||||||
|
Sentry.captureException(error, {
|
||||||
|
tags: {
|
||||||
|
analytics_method: method
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instância global
|
||||||
|
export const analytics = new Analytics({
|
||||||
|
debug: import.meta.env.DEV
|
||||||
|
});
|
||||||
20
src/main.tsx
20
src/main.tsx
@ -5,16 +5,12 @@ 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 { analytics } from './lib/analytics';
|
||||||
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,14 +50,16 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
|||||||
|
|
||||||
const root = createRoot(rootElement);
|
const root = createRoot(rootElement);
|
||||||
|
|
||||||
root.render(
|
// Inicializa o analytics e renderiza o app
|
||||||
|
analytics.init().catch((error) => {
|
||||||
|
console.error('Failed to initialize analytics:', error);
|
||||||
|
// Continua renderizando mesmo se o analytics falhar
|
||||||
|
}).finally(() => {
|
||||||
|
root.render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<GoogleTagManager gtmId={GTM_ID} />
|
<GoogleTagManager gtmId={GTM_ID} />
|
||||||
<RudderstackAnalytics
|
|
||||||
writeKey={RUDDERSTACK_WRITE_KEY}
|
|
||||||
dataPlaneUrl={RUDDERSTACK_DATA_PLANE_URL}
|
|
||||||
/>
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
54
src/types/analytics.ts
Normal file
54
src/types/analytics.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { EVENT_CATEGORIES, EVENT_PROPERTIES, PROPERTY_VALUES } from '../constants/analytics';
|
||||||
|
|
||||||
|
export type EventCategory = typeof EVENT_CATEGORIES[keyof typeof EVENT_CATEGORIES];
|
||||||
|
export type PropertyName = typeof EVENT_PROPERTIES[keyof typeof EVENT_PROPERTIES];
|
||||||
|
export type UserType = typeof PROPERTY_VALUES.USER_TYPES[keyof typeof PROPERTY_VALUES.USER_TYPES];
|
||||||
|
export type InteractionType = typeof PROPERTY_VALUES.INTERACTION_TYPES[keyof typeof PROPERTY_VALUES.INTERACTION_TYPES];
|
||||||
|
export type ErrorType = typeof PROPERTY_VALUES.ERROR_TYPES[keyof typeof PROPERTY_VALUES.ERROR_TYPES];
|
||||||
|
export type ElementSize = typeof PROPERTY_VALUES.ELEMENT_SIZES[keyof typeof PROPERTY_VALUES.ELEMENT_SIZES];
|
||||||
|
export type ElementVariant = typeof PROPERTY_VALUES.ELEMENT_VARIANTS[keyof typeof PROPERTY_VALUES.ELEMENT_VARIANTS];
|
||||||
|
|
||||||
|
export interface BaseEventProperties {
|
||||||
|
category?: EventCategory;
|
||||||
|
action?: string;
|
||||||
|
label?: string;
|
||||||
|
value?: number;
|
||||||
|
interaction_type?: InteractionType;
|
||||||
|
error_type?: ErrorType;
|
||||||
|
user_type?: UserType;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ButtonTrackingOptions {
|
||||||
|
category?: EventCategory;
|
||||||
|
element_type?: string;
|
||||||
|
variant?: ElementVariant;
|
||||||
|
size?: ElementSize;
|
||||||
|
position?: string;
|
||||||
|
section?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormTrackingOptions {
|
||||||
|
category?: EventCategory;
|
||||||
|
form_id?: string;
|
||||||
|
form_name?: string;
|
||||||
|
form_type?: string;
|
||||||
|
form_step?: string;
|
||||||
|
form_status?: string;
|
||||||
|
fields?: Record<string, unknown>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserTraits {
|
||||||
|
id?: string;
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
type?: UserType;
|
||||||
|
role?: string;
|
||||||
|
school_id?: string;
|
||||||
|
class_id?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user