16. Análise de Dados - Parte 1: Fundamentos e Implementação
Este documento apresenta estratégias e práticas para implementar análise de dados em soluções web desenvolvidas com Astro 5, permitindo coletar, processar e visualizar dados de usuários de forma ética e eficiente.
Fundamentos da Análise de Dados
Conceitos Básicos
- Análise de Dados: Processo de inspeção, limpeza, transformação e modelagem de dados para descobrir informações úteis e apoiar a tomada de decisões.
- Métricas: Valores quantificáveis que permitem medir o desempenho de um site ou aplicação.
- KPIs (Key Performance Indicators): Indicadores-chave de desempenho que medem o sucesso em relação a objetivos específicos.
- Eventos: Ações realizadas pelos usuários que são registradas para análise posterior.
- Funil de Conversão: Sequência de etapas que os usuários percorrem até realizar uma ação desejada.
Tipos de Dados para Análise
- Dados de Comportamento: Como os usuários interagem com o site (cliques, navegação, tempo de permanência).
- Dados de Performance: Velocidade de carregamento, tempos de resposta, erros.
- Dados Demográficos: Informações sobre os usuários (localização, dispositivo, navegador).
- Dados de Conversão: Ações que representam valor para o negócio (cadastros, compras, downloads).
Benefícios da Análise de Dados
- Tomada de Decisão Baseada em Dados: Fundamentar decisões em evidências concretas.
- Otimização da Experiência do Usuário: Identificar pontos de atrito e oportunidades de melhoria.
- Personalização: Adaptar o conteúdo e funcionalidades às necessidades dos usuários.
- Medição de ROI: Avaliar o retorno sobre investimento de iniciativas de marketing e desenvolvimento.
Implementação com Astro
Estratégias de Coleta de Dados
1. Integração com Serviços de Análise
Google Analytics 4
---const GA_MEASUREMENT_ID = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;---
{GA_MEASUREMENT_ID && ( <Fragment> <script type="text/partytown" async src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`} ></script> <script type="text/partytown" define:vars={{ GA_MEASUREMENT_ID }}> window.dataLayer = window.dataLayer || []; function gtag() { dataLayer.push(arguments); } gtag('js', new Date()); gtag('config', GA_MEASUREMENT_ID, { send_page_view: false, // Configuramos manualmente para SPA });
// Enviar pageview inicial gtag('event', 'page_view', { page_title: document.title, page_location: window.location.href, page_path: window.location.pathname });
// Monitorar mudanças de rota em SPAs document.addEventListener('astro:page-load', () => { gtag('event', 'page_view', { page_title: document.title, page_location: window.location.href, page_path: window.location.pathname }); }); </script> </Fragment>)}Plausible Analytics (Alternativa Focada em Privacidade)
---const PLAUSIBLE_DOMAIN = import.meta.env.PUBLIC_PLAUSIBLE_DOMAIN;---
{PLAUSIBLE_DOMAIN && ( <script defer data-domain={PLAUSIBLE_DOMAIN} src="https://plausible.io/js/script.js" ></script>)}2. Implementação Própria com Endpoint de API
<script> class EventTracker { constructor() { this.queue = []; this.flushInterval = 10000; // 10 segundos this.endpoint = '/api/analytics'; this.sessionId = this.generateSessionId(); this.clientId = this.getClientId();
// Iniciar flush periódico setInterval(() => this.flush(), this.flushInterval);
// Enviar eventos pendentes antes do usuário sair da página window.addEventListener('beforeunload', () => this.flush());
// Registrar pageview inicial this.trackPageView();
// Monitorar mudanças de página em SPAs document.addEventListener('astro:page-load', () => this.trackPageView()); }
generateSessionId() { return `${Date.now()}-${Math.random().toString(36).substring(2, 12)}`; }
getClientId() { let clientId = localStorage.getItem('analytics_client_id'); if (!clientId) { clientId = `${Date.now()}-${Math.random().toString(36).substring(2, 16)}`; localStorage.setItem('analytics_client_id', clientId); } return clientId; }
trackEvent(eventName, eventData = {}) { const event = { name: eventName, data: eventData, timestamp: new Date().toISOString(), url: window.location.href, referrer: document.referrer, sessionId: this.sessionId, clientId: this.clientId, userAgent: navigator.userAgent, screenSize: `${window.innerWidth}x${window.innerHeight}`, language: navigator.language, };
this.queue.push(event);
// Flush imediatamente se for um evento importante if (eventName === 'conversion' || eventName === 'signup' || eventName === 'purchase') { this.flush(); } }
trackPageView() { this.trackEvent('pageview', { title: document.title, path: window.location.pathname, }); }
async flush() { if (this.queue.length === 0) return;
const events = [...this.queue]; this.queue = [];
try { const response = await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ events }), // Usar keepalive para garantir que a requisição seja concluída // mesmo se o usuário navegar para outra página keepalive: true, });
if (!response.ok) { throw new Error(`Analytics API responded with ${response.status}`); } } catch (error) { console.error('Failed to send analytics events:', error); // Recolocar eventos na fila para tentar novamente this.queue = [...events, ...this.queue]; } } }
// Inicializar e expor globalmente window.eventTracker = new EventTracker();
// API simplificada window.trackEvent = (name, data) => { window.eventTracker.trackEvent(name, data); };</script>Endpoint de API para receber eventos:
export async function post({ request }) { try { const { events } = await request.json();
// Validar dados recebidos if (!Array.isArray(events)) { return new Response(JSON.stringify({ error: 'Invalid events format' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); }
// Processar eventos (exemplo: salvar em banco de dados) await processEvents(events);
return new Response(JSON.stringify({ success: true, count: events.length }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error('Error processing analytics events:', error);
return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); }}
async function processEvents(events) { // Implementação depende do seu backend // Exemplos: // - Salvar em banco de dados (Supabase, Firebase, MongoDB, etc.) // - Enviar para serviço de streaming (Kafka, Kinesis, etc.) // - Armazenar em arquivo de log
// Exemplo com Supabase if (import.meta.env.SUPABASE_URL && import.meta.env.SUPABASE_KEY) { const { createClient } = await import('@supabase/supabase-js'); const supabase = createClient( import.meta.env.SUPABASE_URL, import.meta.env.SUPABASE_KEY );
const { error } = await supabase .from('analytics_events') .insert(events);
if (error) throw error; }}Integração no Layout Principal
---import Analytics from '../components/Analytics.astro';import EventTracker from '../components/EventTracker.astro';import PlausibleAnalytics from '../components/PlausibleAnalytics.astro';
const { title, description } = Astro.props;---
<html lang="pt-BR"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{title}</title> <meta name="description" content={description}>
<!-- Componentes de Analytics --> <Analytics /> <PlausibleAnalytics />
<slot name="head" /></head><body> <header> <!-- Cabeçalho do site --> </header>
<main> <slot /> </main>
<footer> <!-- Rodapé do site --> </footer>
<!-- Rastreador de eventos personalizado --> <EventTracker /></body></html>Rastreamento de Eventos Personalizados
Rastreamento de Interações do Usuário
<form id="contact-form" class="contact-form"> <div class="form-group"> <label for="name">Nome</label> <input type="text" id="name" name="name" required> </div>
<div class="form-group"> <label for="email">E-mail</label> <input type="email" id="email" name="email" required> </div>
<div class="form-group"> <label for="message">Mensagem</label> <textarea id="message" name="message" rows="5" required></textarea> </div>
<button type="submit">Enviar Mensagem</button></form>
<script> const form = document.getElementById('contact-form');
if (form) { // Rastrear visualização do formulário window.trackEvent('form_view', { form_id: 'contact' });
// Rastrear início do preenchimento const inputs = form.querySelectorAll('input, textarea'); inputs.forEach(input => { input.addEventListener('focus', () => { if (!input.dataset.focused) { window.trackEvent('form_field_focus', { form_id: 'contact', field_id: input.id }); input.dataset.focused = 'true'; } }); });
// Rastrear envio do formulário form.addEventListener('submit', (event) => { event.preventDefault();
const formData = new FormData(form); const formValues = Object.fromEntries(formData.entries());
// Enviar dados do formulário para o servidor fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(formValues), }) .then(response => response.json()) .then(data => { // Rastrear sucesso window.trackEvent('form_submit', { form_id: 'contact', success: true });
// Mostrar mensagem de sucesso alert('Mensagem enviada com sucesso!'); form.reset(); }) .catch(error => { // Rastrear erro window.trackEvent('form_submit', { form_id: 'contact', success: false, error: error.message });
// Mostrar mensagem de erro alert('Erro ao enviar mensagem. Por favor, tente novamente.'); }); }); }</script>Rastreamento de Engajamento com Conteúdo
---const { article } = Astro.props;---
<article class="article-content" data-article-id={article.id}> <h1>{article.title}</h1> <p class="article-meta"> Publicado em {article.publishDate} • {article.readingTime} min de leitura </p>
<div class="article-body"> <slot /> </div></article>
<script> document.addEventListener('DOMContentLoaded', () => { const article = document.querySelector('.article-content'); if (!article) return;
const articleId = article.dataset.articleId; const articleTitle = article.querySelector('h1')?.textContent || '';
// Rastrear visualização do artigo window.trackEvent('article_view', { article_id: articleId, article_title: articleTitle, });
// Rastrear progresso de leitura const articleBody = article.querySelector('.article-body'); if (!articleBody) return;
let readingProgress = 0; let maxProgress = 0; let timeOnPage = 0; let intervalId;
// Iniciar timer intervalId = setInterval(() => { timeOnPage += 5;
// Enviar evento a cada 30 segundos if (timeOnPage % 30 === 0) { window.trackEvent('article_engagement', { article_id: articleId, time_on_page: timeOnPage, max_scroll_percentage: maxProgress, }); } }, 5000);
// Rastrear scroll window.addEventListener('scroll', () => { const rect = articleBody.getBoundingClientRect(); const windowHeight = window.innerHeight;
// Calcular quanto do artigo já foi visualizado if (rect.top < windowHeight && rect.bottom > 0) { const totalHeight = rect.height; const visibleHeight = Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0); const visiblePercentage = Math.round((visibleHeight / totalHeight) * 100);
// Atualizar progresso máximo if (visiblePercentage > maxProgress) { maxProgress = visiblePercentage;
// Rastrear marcos de progresso (25%, 50%, 75%, 100%) if (maxProgress >= 25 && readingProgress < 25) { readingProgress = 25; trackReadingMilestone(25); } else if (maxProgress >= 50 && readingProgress < 50) { readingProgress = 50; trackReadingMilestone(50); } else if (maxProgress >= 75 && readingProgress < 75) { readingProgress = 75; trackReadingMilestone(75); } else if (maxProgress >= 90 && readingProgress < 90) { readingProgress = 90; trackReadingMilestone(90); } } } });
function trackReadingMilestone(percentage) { window.trackEvent('article_milestone', { article_id: articleId, percentage: percentage, time_on_page: timeOnPage, }); }
// Limpar timer quando o usuário sai da página window.addEventListener('beforeunload', () => { clearInterval(intervalId);
// Enviar evento final window.trackEvent('article_exit', { article_id: articleId, time_on_page: timeOnPage, max_scroll_percentage: maxProgress, }); }); });</script>Continua na Parte 2: Processamento, Visualização e Privacidade de Dados