feat: Melhorando tracking com o Rudderstack
Some checks are pending
Docker Build and Push / build (push) Waiting to run

This commit is contained in:
Lucas Santana 2025-01-17 11:14:05 -03:00
parent 09c4894a1c
commit 41a225d460
18 changed files with 671 additions and 190 deletions

View File

@ -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;
}

View File

@ -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'
}}

View File

@ -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,
}}

View File

@ -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'

View File

@ -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);
};

View 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>
);
}

View 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>
);
}

View File

@ -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, '')),

View File

@ -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
View 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;

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,
};
}

View File

@ -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
View 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
});

View File

@ -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
View 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;
}