mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37: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 { processAudio } from '../../services/audioService';
|
||||
import { Button } from '../ui/button';
|
||||
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||
|
||||
interface AudioUploaderProps {
|
||||
storyId: string;
|
||||
@ -59,7 +60,7 @@ export function AudioUploader({
|
||||
className="cursor-pointer"
|
||||
trackingId="audio-upload-button"
|
||||
trackingProperties={{
|
||||
category: 'audio',
|
||||
category: EVENT_CATEGORIES.AUDIO,
|
||||
action: 'upload_click',
|
||||
label: 'audio_uploader'
|
||||
}}
|
||||
|
||||
@ -7,6 +7,7 @@ import { useDataLayer } from '../../hooks/useDataLayer';
|
||||
import { useFormTracking } from '../../hooks/useFormTracking';
|
||||
import { Button } from '../ui/button';
|
||||
import { useErrorTracking } from '../../hooks/useErrorTracking';
|
||||
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||
|
||||
interface LoginFormProps {
|
||||
userType: 'school' | 'teacher' | 'student';
|
||||
@ -224,7 +225,7 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
||||
variant="primary"
|
||||
size="lg"
|
||||
trackingProperties={{
|
||||
category: 'auth',
|
||||
category: EVENT_CATEGORIES.AUTH,
|
||||
action: 'login_attempt',
|
||||
label: `${userType}_login`,
|
||||
value: 1,
|
||||
@ -252,7 +253,7 @@ export function LoginForm({ userType, onLogin, onRegisterClick }: LoginFormProps
|
||||
size="sm"
|
||||
onClick={onRegisterClick}
|
||||
trackingProperties={{
|
||||
category: 'auth',
|
||||
category: EVENT_CATEGORIES.AUTH,
|
||||
action: 'register_click',
|
||||
label: userType,
|
||||
}}
|
||||
|
||||
@ -16,6 +16,7 @@ import { ProcessStep } from '@/components/ui/process-step';
|
||||
import { InfoCard } from '@/components/ui/info-card';
|
||||
import { ComparisonSection } from '@/components/ui/comparison-section';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Início', href: '/' },
|
||||
@ -55,7 +56,7 @@ export function HomePage() {
|
||||
variant="ghost"
|
||||
trackingId="nav_login_button"
|
||||
trackingProperties={{
|
||||
category: 'navigation',
|
||||
category: EVENT_CATEGORIES.NAVIGATION,
|
||||
action: 'click',
|
||||
label: 'login_dropdown'
|
||||
}}
|
||||
@ -71,7 +72,7 @@ export function HomePage() {
|
||||
className="w-full text-left"
|
||||
trackingId="nav_school_login"
|
||||
trackingProperties={{
|
||||
category: 'navigation',
|
||||
category: EVENT_CATEGORIES.NAVIGATION,
|
||||
action: 'click',
|
||||
label: 'school_login'
|
||||
}}
|
||||
@ -84,7 +85,7 @@ export function HomePage() {
|
||||
className="w-full text-left"
|
||||
trackingId="nav_teacher_login"
|
||||
trackingProperties={{
|
||||
category: 'navigation',
|
||||
category: EVENT_CATEGORIES.NAVIGATION,
|
||||
action: 'click',
|
||||
label: 'teacher_login'
|
||||
}}
|
||||
@ -97,7 +98,7 @@ export function HomePage() {
|
||||
className="w-full text-left"
|
||||
trackingId="nav_student_login"
|
||||
trackingProperties={{
|
||||
category: 'navigation',
|
||||
category: EVENT_CATEGORIES.NAVIGATION,
|
||||
action: 'click',
|
||||
label: 'student_login'
|
||||
}}
|
||||
@ -113,7 +114,7 @@ export function HomePage() {
|
||||
variant="primary"
|
||||
trackingId="nav_register_button"
|
||||
trackingProperties={{
|
||||
category: 'navigation',
|
||||
category: EVENT_CATEGORIES.NAVIGATION,
|
||||
action: 'click',
|
||||
label: 'register_school'
|
||||
}}
|
||||
@ -149,7 +150,7 @@ export function HomePage() {
|
||||
className="gap-2"
|
||||
trackingId="hero_register_button"
|
||||
trackingProperties={{
|
||||
category: 'hero',
|
||||
category: EVENT_CATEGORIES.HERO,
|
||||
action: 'click',
|
||||
label: 'start_free',
|
||||
position: 'hero_section'
|
||||
@ -165,7 +166,7 @@ export function HomePage() {
|
||||
className="gap-2"
|
||||
trackingId="hero_demo_button"
|
||||
trackingProperties={{
|
||||
category: 'hero',
|
||||
category: EVENT_CATEGORIES.HERO,
|
||||
action: 'click',
|
||||
label: 'watch_demo',
|
||||
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"
|
||||
trackingId="hero_video_play"
|
||||
trackingProperties={{
|
||||
category: 'hero',
|
||||
category: EVENT_CATEGORIES.HERO,
|
||||
action: 'click',
|
||||
label: 'play_demo_video',
|
||||
position: 'hero_video'
|
||||
|
||||
@ -1,20 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useButtonTracking } from '../../hooks/useButtonTracking';
|
||||
import { ButtonTrackingOptions } from '../../types/analytics';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
as?: 'button' | 'span';
|
||||
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;
|
||||
};
|
||||
trackingProperties?: ButtonTrackingOptions;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
@ -31,18 +26,18 @@ export function Button({
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const { trackButtonClick } = useButtonTracking({
|
||||
category: trackingProperties?.category || 'interaction'
|
||||
category: EVENT_CATEGORIES.INTERACTION,
|
||||
element_type: 'button',
|
||||
...trackingProperties
|
||||
});
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
// Rastreia o clique
|
||||
trackButtonClick(trackingId, {
|
||||
variant,
|
||||
size,
|
||||
...trackingProperties,
|
||||
});
|
||||
|
||||
// Chama o onClick original se existir
|
||||
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 { CheckCircle } from 'lucide-react';
|
||||
import { Button } from './button';
|
||||
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||
|
||||
interface PlanProps {
|
||||
title: string;
|
||||
@ -120,7 +121,7 @@ export function PlanForParents({
|
||||
className="w-full"
|
||||
trackingId={`parent_plan_subscribe_${plan.title.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
trackingProperties={{
|
||||
category: 'pricing',
|
||||
category: EVENT_CATEGORIES.PRICING,
|
||||
action: 'click',
|
||||
label: plan.title,
|
||||
value: parseFloat(plan.price.replace(/[.,]/g, '')),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { CheckCircle } from 'lucide-react';
|
||||
import { Button } from './button';
|
||||
import { EVENT_CATEGORIES } from '../../constants/analytics';
|
||||
|
||||
interface PlanProps {
|
||||
title: string;
|
||||
@ -109,7 +110,7 @@ export function PlanForSchools({
|
||||
className="w-full"
|
||||
trackingId={`plan_subscribe_${plan.title.toLowerCase().replace(/\s+/g, '_')}`}
|
||||
trackingProperties={{
|
||||
category: 'pricing',
|
||||
category: EVENT_CATEGORIES.PRICING,
|
||||
action: 'click',
|
||||
label: plan.title,
|
||||
value: parseFloat(plan.price.replace(/[.,]/g, '')),
|
||||
@ -138,7 +139,7 @@ export function PlanForSchools({
|
||||
className="gap-2"
|
||||
trackingId="pricing_contact_button"
|
||||
trackingProperties={{
|
||||
category: 'pricing',
|
||||
category: EVENT_CATEGORIES.PRICING,
|
||||
action: 'click',
|
||||
label: 'contact_sales',
|
||||
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 {
|
||||
category?: string;
|
||||
location?: string;
|
||||
}
|
||||
|
||||
interface ButtonTrackingProperties {
|
||||
label?: string;
|
||||
value?: string | number;
|
||||
action?: string;
|
||||
variant?: string;
|
||||
position?: string;
|
||||
// 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 { 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;
|
||||
}
|
||||
properties?: ButtonTrackingProperties
|
||||
) => {
|
||||
track('button_clicked', {
|
||||
analytics.track('button_clicked', {
|
||||
button_id: buttonId,
|
||||
category,
|
||||
location,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { useRudderstack } from './useRudderstack';
|
||||
import { analytics } from '../lib/analytics';
|
||||
|
||||
interface ErrorTrackingOptions {
|
||||
category?: string;
|
||||
@ -8,7 +8,6 @@ interface ErrorTrackingOptions {
|
||||
}
|
||||
|
||||
export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
||||
const { track } = useRudderstack();
|
||||
const { category = 'error', userId, userEmail } = options;
|
||||
|
||||
const trackError = (
|
||||
@ -34,7 +33,7 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
||||
});
|
||||
|
||||
// 2. Rastreia no Rudderstack (para analytics)
|
||||
track('error_occurred', {
|
||||
analytics.track('error_occurred', {
|
||||
category,
|
||||
error_name: error.name,
|
||||
error_message: error.message,
|
||||
@ -64,7 +63,7 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
||||
});
|
||||
|
||||
// 2. Rastreia no Rudderstack
|
||||
track('error_boundary_triggered', {
|
||||
analytics.track('error_boundary_triggered', {
|
||||
category,
|
||||
error_name: error.name,
|
||||
error_message: error.message,
|
||||
@ -93,7 +92,7 @@ export function useErrorTracking(options: ErrorTrackingOptions = {}) {
|
||||
});
|
||||
|
||||
// 2. Rastreia no Rudderstack
|
||||
track('api_error_occurred', {
|
||||
analytics.track('api_error_occurred', {
|
||||
category,
|
||||
endpoint,
|
||||
method,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useRudderstack } from './useRudderstack';
|
||||
import { analytics } from '../lib/analytics';
|
||||
|
||||
interface FormTrackingOptions {
|
||||
formId: string;
|
||||
@ -7,10 +7,8 @@ interface FormTrackingOptions {
|
||||
}
|
||||
|
||||
export function useFormTracking({ formId, formName, category = 'form' }: FormTrackingOptions) {
|
||||
const { track } = useRudderstack();
|
||||
|
||||
const trackFormStarted = () => {
|
||||
track('form_started', {
|
||||
analytics.track('form_started', {
|
||||
form_id: formId,
|
||||
form_name: formName,
|
||||
category,
|
||||
@ -18,7 +16,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
||||
};
|
||||
|
||||
const trackFormStepCompleted = (step: string, isValid: boolean) => {
|
||||
track('form_step_completed', {
|
||||
analytics.track('form_step_completed', {
|
||||
form_id: formId,
|
||||
form_name: formName,
|
||||
category,
|
||||
@ -28,7 +26,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
||||
};
|
||||
|
||||
const trackFormSubmitted = (isValid: boolean, fields?: Record<string, any>) => {
|
||||
track('form_submitted', {
|
||||
analytics.track('form_submitted', {
|
||||
form_id: formId,
|
||||
form_name: formName,
|
||||
category,
|
||||
@ -38,7 +36,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
||||
};
|
||||
|
||||
const trackFormError = (errorType: string, errorMessage: string, field?: string) => {
|
||||
track('form_error', {
|
||||
analytics.track('form_error', {
|
||||
form_id: formId,
|
||||
form_name: formName,
|
||||
category,
|
||||
@ -49,7 +47,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
||||
};
|
||||
|
||||
const trackFormAbandoned = (step?: string) => {
|
||||
track('form_abandoned', {
|
||||
analytics.track('form_abandoned', {
|
||||
form_id: formId,
|
||||
form_name: formName,
|
||||
category,
|
||||
@ -62,7 +60,7 @@ export function useFormTracking({ formId, formName, category = 'form' }: FormTra
|
||||
interactionType: 'focus' | 'blur' | 'change',
|
||||
value?: string
|
||||
) => {
|
||||
track('form_field_interaction', {
|
||||
analytics.track('form_field_interaction', {
|
||||
form_id: formId,
|
||||
form_name: formName,
|
||||
category,
|
||||
|
||||
@ -1,47 +1,25 @@
|
||||
interface RudderstackEvent {
|
||||
event: string;
|
||||
properties?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface UserTraits {
|
||||
[key: string]: any;
|
||||
}
|
||||
import { analytics } from '../lib/analytics';
|
||||
import { UserTraits } from '../types/analytics';
|
||||
|
||||
export function useRudderstack() {
|
||||
const track = (eventName: string, properties?: Record<string, any>) => {
|
||||
if (window.rudderanalytics) {
|
||||
window.rudderanalytics.track(eventName, properties);
|
||||
}
|
||||
const track = (eventName: string, properties?: Record<string, unknown>) => {
|
||||
analytics.track(eventName, properties);
|
||||
};
|
||||
|
||||
const page = (name?: string, properties?: Record<string, any>) => {
|
||||
if (window.rudderanalytics) {
|
||||
window.rudderanalytics.page(name, properties);
|
||||
}
|
||||
const page = (name?: string, properties?: Record<string, unknown>) => {
|
||||
analytics.page(name, properties);
|
||||
};
|
||||
|
||||
const identify = (userId: string, traits?: UserTraits) => {
|
||||
if (window.rudderanalytics) {
|
||||
window.rudderanalytics.identify(userId, traits);
|
||||
}
|
||||
analytics.identify({ id: userId, user_metadata: traits } as any);
|
||||
};
|
||||
|
||||
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 group = (groupId: string, traits?: Record<string, unknown>) => {
|
||||
analytics.group(groupId, traits);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
if (window.rudderanalytics) {
|
||||
window.rudderanalytics.reset();
|
||||
}
|
||||
analytics.reset();
|
||||
};
|
||||
|
||||
return {
|
||||
@ -49,7 +27,6 @@ export function useRudderstack() {
|
||||
page,
|
||||
identify,
|
||||
group,
|
||||
alias,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { useRudderstack } from './useRudderstack';
|
||||
import { analytics } from '../lib/analytics';
|
||||
|
||||
interface StoryGeneratedProps {
|
||||
story_id: string;
|
||||
@ -47,38 +47,36 @@ interface InterestActionProps {
|
||||
}
|
||||
|
||||
export function useStudentTracking() {
|
||||
const { track } = useRudderstack();
|
||||
|
||||
const trackStoryGenerated = (properties: StoryGeneratedProps) => {
|
||||
track('story_generated', {
|
||||
analytics.track('story_generated', {
|
||||
...properties,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
const trackAudioRecorded = (properties: AudioRecordedProps) => {
|
||||
track('audio_recorded', {
|
||||
analytics.track('audio_recorded', {
|
||||
...properties,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
const trackExerciseCompleted = (properties: ExerciseCompletedProps) => {
|
||||
track('exercise_completed', {
|
||||
analytics.track('exercise_completed', {
|
||||
...properties,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
const trackInterestAdded = (properties: InterestActionProps) => {
|
||||
track('interest_added', {
|
||||
analytics.track('interest_added', {
|
||||
...properties,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
};
|
||||
|
||||
const trackInterestRemoved = (properties: InterestActionProps) => {
|
||||
track('interest_removed', {
|
||||
analytics.track('interest_removed', {
|
||||
...properties,
|
||||
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
|
||||
});
|
||||
30
src/main.tsx
30
src/main.tsx
@ -5,16 +5,12 @@ 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 { analytics } from './lib/analytics';
|
||||
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,14 +50,16 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
||||
|
||||
const root = createRoot(rootElement);
|
||||
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GoogleTagManager gtmId={GTM_ID} />
|
||||
<RudderstackAnalytics
|
||||
writeKey={RUDDERSTACK_WRITE_KEY}
|
||||
dataPlaneUrl={RUDDERSTACK_DATA_PLANE_URL}
|
||||
/>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
// 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}>
|
||||
<GoogleTagManager gtmId={GTM_ID} />
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
</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