Pular para o conteúdo

16. Análise de Dados - Parte 3: Visualização e Privacidade

Esta seção aborda a visualização dos dados coletados e processados, bem como as práticas de privacidade e conformidade em soluções web desenvolvidas com Astro 5.

Visualização de Dados

Dashboard Interno

Componente de Dashboard

src/pages/admin/analytics/index.astro
---
import AdminLayout from '../../../layouts/AdminLayout.astro';
import AnalyticsSummary from '../../../components/analytics/AnalyticsSummary.astro';
import PageviewsChart from '../../../components/analytics/PageviewsChart.astro';
import TopPagesTable from '../../../components/analytics/TopPagesTable.astro';
import EventsChart from '../../../components/analytics/EventsChart.astro';
import ConversionFunnel from '../../../components/analytics/ConversionFunnel.astro';
// Verificar autenticação
if (!Astro.locals.user || !Astro.locals.user.isAdmin) {
return Astro.redirect('/login?redirect=/admin/analytics');
}
// Parâmetros de data
const searchParams = Astro.url.searchParams;
const period = searchParams.get('period') || '7d';
let startDate, endDate;
const now = new Date();
endDate = new Date(now);
switch (period) {
case '1d':
startDate = new Date(now);
startDate.setDate(startDate.getDate() - 1);
break;
case '7d':
startDate = new Date(now);
startDate.setDate(startDate.getDate() - 7);
break;
case '30d':
startDate = new Date(now);
startDate.setDate(startDate.getDate() - 30);
break;
case '90d':
startDate = new Date(now);
startDate.setDate(startDate.getDate() - 90);
break;
default:
startDate = new Date(now);
startDate.setDate(startDate.getDate() - 7);
}
---
<AdminLayout title="Analytics Dashboard">
<div class="analytics-dashboard">
<header class="dashboard-header">
<h1>Analytics Dashboard</h1>
<div class="period-selector">
<a href="?period=1d" class={period === '1d' ? 'active' : ''}>Hoje</a>
<a href="?period=7d" class={period === '7d' ? 'active' : ''}>7 dias</a>
<a href="?period=30d" class={period === '30d' ? 'active' : ''}>30 dias</a>
<a href="?period=90d" class={period === '90d' ? 'active' : ''}>90 dias</a>
</div>
</header>
<AnalyticsSummary startDate={startDate} endDate={endDate} />
<div class="dashboard-grid">
<div class="dashboard-card full-width">
<h2>Pageviews</h2>
<PageviewsChart startDate={startDate} endDate={endDate} />
</div>
<div class="dashboard-card">
<h2>Top Pages</h2>
<TopPagesTable startDate={startDate} endDate={endDate} limit={10} />
</div>
<div class="dashboard-card">
<h2>Event Distribution</h2>
<EventsChart startDate={startDate} endDate={endDate} />
</div>
<div class="dashboard-card full-width">
<h2>Conversion Funnel</h2>
<ConversionFunnel
startDate={startDate}
endDate={endDate}
steps={[
{ name: 'pageview', label: 'Page View' },
{ name: 'product_view', label: 'Product View' },
{ name: 'add_to_cart', label: 'Add to Cart' },
{ name: 'checkout_start', label: 'Start Checkout' },
{ name: 'purchase', label: 'Purchase' }
]}
/>
</div>
</div>
</div>
</AdminLayout>
<style>
.analytics-dashboard {
padding: 1rem;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.period-selector {
display: flex;
gap: 1rem;
}
.period-selector a {
padding: 0.5rem 1rem;
border-radius: 4px;
text-decoration: none;
color: #333;
background-color: #f0f0f0;
}
.period-selector a.active {
background-color: #4a6cf7;
color: white;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.dashboard-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
}
.full-width {
grid-column: 1 / -1;
}
h1 {
margin: 0;
font-size: 1.8rem;
}
h2 {
margin-top: 0;
font-size: 1.2rem;
color: #666;
}
</style>

Componente de Resumo de Métricas

src/components/analytics/AnalyticsSummary.astro
---
import { getActiveUsers, getBounceRate } from '../../utils/analytics-metrics';
import { getPageviewsByDay, getEventCountsByType } from '../../utils/analytics-aggregation';
const { startDate, endDate } = Astro.props;
// Buscar dados
const [activeUsers, bounceRate, pageviewsByDay, eventCounts] = await Promise.all([
getActiveUsers('day'),
getBounceRate(startDate, endDate),
getPageviewsByDay(startDate, endDate),
getEventCountsByType(startDate, endDate)
]);
// Calcular total de pageviews
const totalPageviews = pageviewsByDay.reduce((sum, day) => sum + day.pageview_count, 0);
// Calcular média diária
const dayCount = Math.max(1, Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)));
const avgDailyPageviews = Math.round(totalPageviews / dayCount);
// Encontrar total de eventos
const totalEvents = eventCounts.reduce((sum, event) => sum + event.event_count, 0);
---
<div class="metrics-summary">
<div class="metric-card">
<div class="metric-value">{activeUsers}</div>
<div class="metric-label">Usuários Ativos Hoje</div>
</div>
<div class="metric-card">
<div class="metric-value">{totalPageviews.toLocaleString()}</div>
<div class="metric-label">Total de Pageviews</div>
</div>
<div class="metric-card">
<div class="metric-value">{avgDailyPageviews.toLocaleString()}</div>
<div class="metric-label">Média Diária</div>
</div>
<div class="metric-card">
<div class="metric-value">{bounceRate}%</div>
<div class="metric-label">Taxa de Rejeição</div>
</div>
</div>
<style>
.metrics-summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 2rem;
}
.metric-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
text-align: center;
}
.metric-value {
font-size: 2rem;
font-weight: bold;
color: #4a6cf7;
margin-bottom: 0.5rem;
}
.metric-label {
font-size: 0.9rem;
color: #666;
}
@media (max-width: 768px) {
.metrics-summary {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.metrics-summary {
grid-template-columns: 1fr;
}
}
</style>

Componente de Gráfico de Pageviews

src/components/analytics/PageviewsChart.astro
---
import { getPageviewsByDay } from '../../utils/analytics-aggregation';
const { startDate, endDate } = Astro.props;
// Buscar dados
const pageviewsByDay = await getPageviewsByDay(startDate, endDate);
// Formatar dados para o gráfico
const chartData = {
labels: pageviewsByDay.map(day => {
const date = new Date(day.day);
return date.toLocaleDateString('pt-BR', { day: 'numeric', month: 'short' });
}),
datasets: [
{
label: 'Pageviews',
data: pageviewsByDay.map(day => day.pageview_count),
backgroundColor: 'rgba(74, 108, 247, 0.2)',
borderColor: 'rgba(74, 108, 247, 1)',
borderWidth: 2,
tension: 0.4,
fill: true,
}
]
};
// ID único para o canvas
const chartId = `pageviews-chart-${Date.now()}`;
---
<div class="chart-container">
<canvas id={chartId}></canvas>
</div>
<script define:vars={{ chartId, chartData }}>
document.addEventListener('DOMContentLoaded', () => {
const ctx = document.getElementById(chartId).getContext('2d');
new Chart(ctx, {
type: 'line',
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false,
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
}
}
});
});
</script>
<style>
.chart-container {
height: 300px;
width: 100%;
}
</style>

Integração com Chart.js

src/layouts/AdminLayout.astro
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{Astro.props.title || 'Admin Dashboard'}</title>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.0.0/dist/chart.umd.min.js"></script>
<slot name="head" />
</head>
<body>
<div class="admin-layout">
<aside class="sidebar">
<nav>
<ul>
<li><a href="/admin">Dashboard</a></li>
<li><a href="/admin/analytics">Analytics</a></li>
<li><a href="/admin/content">Content</a></li>
<li><a href="/admin/users">Users</a></li>
<li><a href="/admin/settings">Settings</a></li>
</ul>
</nav>
</aside>
<main class="content">
<slot />
</main>
</div>
</body>
</html>
<style>
.admin-layout {
display: grid;
grid-template-columns: 250px 1fr;
min-height: 100vh;
}
.sidebar {
background-color: #1a1a2e;
color: white;
padding: 1.5rem;
}
.sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar li {
margin-bottom: 0.5rem;
}
.sidebar a {
color: white;
text-decoration: none;
display: block;
padding: 0.75rem 1rem;
border-radius: 4px;
}
.sidebar a:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.content {
background-color: #f5f5f5;
padding: 1.5rem;
}
</style>

Privacidade e Conformidade

Política de Privacidade

src/pages/privacy.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Política de Privacidade" description="Nossa política de privacidade e uso de dados">
<main class="privacy-policy">
<h1>Política de Privacidade</h1>
<section>
<h2>1. Introdução</h2>
<p>
Esta Política de Privacidade descreve como coletamos, usamos e compartilhamos informações
quando você utiliza nosso site. Respeitamos sua privacidade e estamos comprometidos
com a proteção de seus dados pessoais.
</p>
</section>
<section>
<h2>2. Dados que Coletamos</h2>
<p>Podemos coletar os seguintes tipos de informações:</p>
<h3>2.1. Informações fornecidas por você</h3>
<ul>
<li>Informações de contato (como nome e e-mail) quando você preenche formulários</li>
<li>Informações de conta quando você se registra</li>
<li>Conteúdo que você publica ou compartilha em nosso site</li>
</ul>
<h3>2.2. Informações coletadas automaticamente</h3>
<ul>
<li>Dados de uso e navegação (páginas visitadas, tempo gasto no site)</li>
<li>Informações do dispositivo (tipo de navegador, sistema operacional)</li>
<li>Endereço IP e localização aproximada</li>
<li>Cookies e tecnologias similares</li>
</ul>
</section>
<section>
<h2>3. Como Usamos seus Dados</h2>
<p>Utilizamos suas informações para:</p>
<ul>
<li>Fornecer, manter e melhorar nossos serviços</li>
<li>Processar suas solicitações e transações</li>
<li>Entender como você utiliza nosso site para melhorar a experiência</li>
<li>Personalizar conteúdo e recomendações</li>
<li>Comunicar-nos com você sobre atualizações, ofertas e eventos</li>
<li>Detectar e prevenir fraudes e abusos</li>
</ul>
</section>
<section>
<h2>4. Cookies e Tecnologias Similares</h2>
<p>
Utilizamos cookies e tecnologias similares para coletar informações sobre sua atividade,
navegador e dispositivo. Você pode gerenciar suas preferências de cookies através das
configurações do seu navegador.
</p>
<p>Tipos de cookies que utilizamos:</p>
<ul>
<li><strong>Cookies essenciais:</strong> Necessários para o funcionamento do site</li>
<li><strong>Cookies de preferências:</strong> Permitem lembrar suas escolhas e personalizar o site</li>
<li><strong>Cookies analíticos:</strong> Nos ajudam a entender como os visitantes interagem com o site</li>
<li><strong>Cookies de marketing:</strong> Utilizados para rastrear visitantes em sites</li>
</ul>
</section>
<section>
<h2>5. Seus Direitos</h2>
<p>Dependendo da sua localização, você pode ter os seguintes direitos:</p>
<ul>
<li>Acessar seus dados pessoais</li>
<li>Corrigir dados imprecisos</li>
<li>Excluir seus dados</li>
<li>Restringir ou opor-se ao processamento de seus dados</li>
<li>Portabilidade de dados</li>
<li>Retirar consentimento</li>
</ul>
<p>
Para exercer qualquer um desses direitos, entre em contato conosco através do e-mail:
<a href="mailto:privacy@exemplo.com.br">privacy@exemplo.com.br</a>
</p>
</section>
<section>
<h2>6. Segurança de Dados</h2>
<p>
Implementamos medidas de segurança técnicas e organizacionais para proteger seus dados
contra acesso não autorizado, perda ou alteração. No entanto, nenhum método de transmissão
pela Internet ou armazenamento eletrônico é 100% seguro.
</p>
</section>
<section>
<h2>7. Alterações nesta Política</h2>
<p>
Podemos atualizar esta Política de Privacidade periodicamente. A versão mais recente
estará sempre disponível nesta página, com a data da última atualização.
</p>
</section>
<section>
<h2>8. Contato</h2>
<p>
Se você tiver dúvidas sobre esta Política de Privacidade ou sobre como tratamos seus dados,
entre em contato conosco:
</p>
<address>
<strong>E-mail:</strong> <a href="mailto:privacy@exemplo.com.br">privacy@exemplo.com.br</a><br>
<strong>Endereço:</strong> Av. Exemplo, 123 - São Paulo, SP - Brasil
</address>
</section>
<footer>
<p>Última atualização: 21 de março de 2025</p>
</footer>
</main>
</BaseLayout>
<style>
.privacy-policy {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
}
h1 {
font-size: 2rem;
margin-bottom: 2rem;
color: #333;
}
h2 {
font-size: 1.5rem;
margin: 2rem 0 1rem;
color: #444;
}
h3 {
font-size: 1.2rem;
margin: 1.5rem 0 0.75rem;
color: #555;
}
section {
margin-bottom: 2rem;
}
ul {
margin-left: 1.5rem;
}
li {
margin-bottom: 0.5rem;
}
footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid #eee;
font-style: italic;
color: #777;
}
</style>

Componente de Consentimento de Cookies

src/components/CookieConsent.astro
<div id="cookie-consent" class="cookie-banner hidden">
<div class="cookie-content">
<h2>Utilizamos Cookies</h2>
<p>
Este site utiliza cookies para melhorar sua experiência. Ao continuar navegando,
você concorda com nossa <a href="/privacy">Política de Privacidade</a>.
</p>
<div class="cookie-settings">
<details>
<summary>Configurações Avançadas</summary>
<div class="cookie-options">
<label>
<input type="checkbox" name="essential" checked disabled>
<span>Essenciais (obrigatórios)</span>
</label>
<label>
<input type="checkbox" name="preferences" checked>
<span>Preferências</span>
</label>
<label>
<input type="checkbox" name="analytics" checked>
<span>Análise</span>
</label>
<label>
<input type="checkbox" name="marketing">
<span>Marketing</span>
</label>
</div>
</details>
</div>
<div class="cookie-actions">
<button id="reject-cookies" class="btn-secondary">Rejeitar Não-Essenciais</button>
<button id="accept-cookies" class="btn-primary">Aceitar Todos</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const cookieBanner = document.getElementById('cookie-consent');
const acceptButton = document.getElementById('accept-cookies');
const rejectButton = document.getElementById('reject-cookies');
const cookieOptions = document.querySelectorAll('.cookie-options input[type="checkbox"]');
// Verificar se já existe consentimento
const hasConsent = localStorage.getItem('cookie-consent');
if (!hasConsent) {
// Mostrar banner após um pequeno delay
setTimeout(() => {
cookieBanner.classList.remove('hidden');
}, 1000);
} else {
// Aplicar configurações salvas
const savedSettings = JSON.parse(localStorage.getItem('cookie-settings') || '{}');
applyCookieSettings(savedSettings);
}
// Aceitar todos os cookies
acceptButton.addEventListener('click', () => {
const settings = {
essential: true,
preferences: true,
analytics: true,
marketing: true,
};
saveCookieSettings(settings);
cookieBanner.classList.add('hidden');
});
// Rejeitar cookies não essenciais
rejectButton.addEventListener('click', () => {
const settings = {
essential: true,
preferences: false,
analytics: false,
marketing: false,
};
saveCookieSettings(settings);
cookieBanner.classList.add('hidden');
});
// Salvar configurações personalizadas
function saveCookieSettings(settings) {
localStorage.setItem('cookie-consent', 'true');
localStorage.setItem('cookie-settings', JSON.stringify(settings));
// Aplicar configurações
applyCookieSettings(settings);
// Disparar evento para outros scripts
window.dispatchEvent(new CustomEvent('cookieConsentUpdate', { detail: settings }));
}
// Aplicar configurações de cookies
function applyCookieSettings(settings) {
// Desativar scripts de analytics se não permitido
if (!settings.analytics) {
disableAnalytics();
}
// Desativar scripts de marketing se não permitido
if (!settings.marketing) {
disableMarketing();
}
}
function disableAnalytics() {
// Desativar Google Analytics
window['ga-disable-UA-XXXXX-Y'] = true;
// Remover scripts de analytics
document.querySelectorAll('script[data-type="analytics"]').forEach(script => {
script.remove();
});
}
function disableMarketing() {
// Remover scripts de marketing
document.querySelectorAll('script[data-type="marketing"]').forEach(script => {
script.remove();
});
}
});
</script>
<style>
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: white;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
padding: 1rem;
transition: transform 0.3s ease-in-out;
}
.cookie-banner.hidden {
transform: translateY(100%);
}
.cookie-content {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.cookie-actions {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.cookie-settings {
margin: 1rem 0;
}
.cookie-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
margin-top: 1rem;
}
.btn-primary {
background-color: #4a6cf7;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
}
.btn-secondary {
background-color: #f0f0f0;
color: #333;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
}
details summary {
cursor: pointer;
color: #4a6cf7;
margin-bottom: 0.5rem;
}
h2 {
margin-top: 0;
font-size: 1.2rem;
}
</style>

Integração com o Componente de Analytics

---
// src/components/Analytics.astro (versão atualizada)
const GA_MEASUREMENT_ID = import.meta.env.PUBLIC_GA_MEASUREMENT_ID;
---
{GA_MEASUREMENT_ID && (
<Fragment>
<!-- Script carregado apenas se o usuário consentir -->
<script
type="text/partytown"
data-type="analytics"
async
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
></script>
<script type="text/partytown" data-type="analytics" define:vars={{ GA_MEASUREMENT_ID }}>
// Verificar consentimento
const cookieSettings = JSON.parse(localStorage.getItem('cookie-settings') || '{}');
if (cookieSettings.analytics) {
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', GA_MEASUREMENT_ID, {
send_page_view: false,
anonymize_ip: true // Anonimizar IPs para GDPR
});
// 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
});
});
}
// Atualizar quando as configurações de cookies mudarem
window.addEventListener('cookieConsentUpdate', (event) => {
const settings = event.detail;
if (settings.analytics) {
// Reativar analytics
window['ga-disable-' + GA_MEASUREMENT_ID] = false;
// Inicializar se ainda não estiver inicializado
if (!window.dataLayer) {
window.dataLayer = [];
gtag('js', new Date());
gtag('config', GA_MEASUREMENT_ID, {
anonymize_ip: true
});
}
} else {
// Desativar analytics
window['ga-disable-' + GA_MEASUREMENT_ID] = true;
}
});
</script>
</Fragment>
)}

Conclusão

A implementação de uma estratégia abrangente de análise de dados é essencial para entender o comportamento dos usuários e melhorar continuamente sua solução web desenvolvida com Astro 5. Ao seguir as práticas recomendadas neste documento, você estará coletando, processando e visualizando dados de forma ética e eficiente, respeitando a privacidade dos usuários e cumprindo com as regulamentações aplicáveis.

Lembre-se de que a análise de dados é um processo contínuo. À medida que sua solução evolui, você deve revisar e adaptar sua estratégia de análise para garantir que está coletando os dados mais relevantes para seus objetivos de negócio.

Os próximos documentos detalharão outros aspectos do desenvolvimento:


Última atualização: 21 de março de 2025