diff --git a/.gitignore b/.gitignore index 85e3640..a86aee7 100644 --- a/.gitignore +++ b/.gitignore @@ -73,13 +73,6 @@ web_modules/ # Yarn Integrity file .yarn-integrity -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..4fb735c --- /dev/null +++ b/app/.env @@ -0,0 +1,2 @@ +VITE_PLAUSIBLE_API=http://localhost/api/event +VITE_PLAUSIBLE_DOMAIN=myapp-dev diff --git a/app/.env.production b/app/.env.production new file mode 100644 index 0000000..674d2d8 --- /dev/null +++ b/app/.env.production @@ -0,0 +1,2 @@ +VITE_PLAUSIBLE_API=https://plausible.mtst.tec.br/api/event +VITE_PLAUSIBLE_DOMAIN=https://decoda.mtst.tec.br diff --git a/app/Dockerfile b/app/Dockerfile index 4cc47da..51a2723 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -3,7 +3,7 @@ FROM node:20-alpine AS builder WORKDIR /app ARG GIT_COMMIT_HASH=unknown -ARG APP_VERSION=1.1.2 +ARG APP_VERSION=1.2.0 ENV VITE_APP_VERSION=$APP_VERSION ENV VITE_GIT_HASH=$GIT_COMMIT_HASH diff --git a/app/index.html b/app/index.html index 7869230..0de87cf 100644 --- a/app/index.html +++ b/app/index.html @@ -1,14 +1,14 @@ - - - - - - - - Decoda - - -
- - - + + + + + + + + Decoda + + +
+ + + diff --git a/app/package.json b/app/package.json index 7a27d14..90bf92e 100644 --- a/app/package.json +++ b/app/package.json @@ -2,7 +2,7 @@ "name": "decoda", "private": true, "description": "Aplicação educacional desenvolvida para ensino de programação básica e letramento digital", - "version": "1.1.2", + "version": "1.2.0", "main": "main.cjs", "homepage": "./", "type": "module", diff --git a/app/src/App.jsx b/app/src/App.jsx index d56cb15..1675cac 100644 --- a/app/src/App.jsx +++ b/app/src/App.jsx @@ -4,12 +4,13 @@ * @module App */ -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useEffect } from "react"; import { HashRouter as Router, Routes, Route, useLocation } from "react-router-dom"; import "./App.css"; import HomePage from "./pages/HomePage/HomePage"; import LabPython from "./pages/LabPython/LabPython"; import ScrollToTop from "./components/ScrollToTop"; +import { trackPageView } from "./services/plausible"; const Playground = lazy(() => import("./pages/Playground/Playground")); const About = lazy(() => import("./pages/About/About")); @@ -68,6 +69,10 @@ function AppRoutes() { // keeps rendering behind the modal overlay. const backgroundLocation = location.state?.backgroundLocation; + useEffect(() => { + trackPageView(location.pathname); + }, [location.pathname]); + return ( <> diff --git a/app/src/atividades/letramento/shared/letramento.css b/app/src/atividades/letramento/shared/letramento.css index 85645e6..858b0ac 100644 --- a/app/src/atividades/letramento/shared/letramento.css +++ b/app/src/atividades/letramento/shared/letramento.css @@ -653,10 +653,6 @@ video { bottom: 0px; } -.bottom-0 { - bottom: 0px; -} - .bottom-10 { bottom: 2.5rem; } @@ -984,10 +980,6 @@ video { height: 0.25rem; } -.h-1\.5 { - height: 0.375rem; -} - .h-10 { height: 2.5rem; } @@ -2885,10 +2877,6 @@ video { background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1)); } -.bg-gray-200\/50 { - background-color: rgb(229 231 235 / 0.5); -} - .bg-gray-300 { --tw-bg-opacity: 1; background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1)); @@ -7125,10 +7113,6 @@ video { transition-duration: 150ms; } -.duration-1000 { - transition-duration: 1000ms; -} - .duration-150 { transition-duration: 150ms; } @@ -7149,10 +7133,6 @@ video { transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } -.ease-out { - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); -} - .\[-webkit-text-stroke\:2px_black\] { -webkit-text-stroke: 2px black; } diff --git a/app/src/contexts/GameStateContext.jsx b/app/src/contexts/GameStateContext.jsx index c984618..4951e72 100644 --- a/app/src/contexts/GameStateContext.jsx +++ b/app/src/contexts/GameStateContext.jsx @@ -16,6 +16,7 @@ import React, { import PropTypes from "prop-types"; import { gameEventBus } from "../utils/gameEvents"; import { GameProgressProvider, useGameProgress } from "./GameProgressContext"; +import { trackEvent } from "../services/plausible"; export const GAME_STATES = { PARADO: "parado", @@ -89,6 +90,9 @@ function GameStateInnerProvider({ children, gameConfig }) { ); return urlParams.get(debugKey)?.toLowerCase() === "true"; }); + const [phaseStartTime, setPhaseStartTime] = useState(null); + const [attemptCount, setAttemptCount] = useState(0); + const [failureCount, setFailureCount] = useState(0); const getCodeFromWorkspace = useRef(null); const getCodeFromEditor = useRef(null); @@ -103,6 +107,8 @@ function GameStateInnerProvider({ children, gameConfig }) { * @throws {console.error} Se editor não registrou a função de execução */ const execute = () => { + setAttemptCount(prev => prev + 1); + if (editorType === "code") { if (getCodeFromEditor.current) { const codigo = getCodeFromEditor.current(); @@ -145,6 +151,19 @@ function GameStateInnerProvider({ children, gameConfig }) { if (!completedPhases.includes(currentPhase)) { setCompletedPhases([...completedPhases, currentPhase]); } + + const durationSeconds = phaseStartTime + ? Math.round((Date.now() - phaseStartTime) / 1000) + : null; + + trackEvent('Activity Success', { + activity: gameConfig.gameId, + phase: currentPhase, + blocks: currentBlockCount, + attempts: attemptCount, + failures: failureCount, + durationSeconds, + }); }; /** @@ -157,6 +176,14 @@ function GameStateInnerProvider({ children, gameConfig }) { */ const finalizeWithFailure = () => { setExecutionState(GAME_STATES.FALHA); + setFailureCount(prev => prev + 1); + + trackEvent('Activity Failure', { + activity: gameConfig.gameId, + phase: currentPhase, + blocks: currentBlockCount, + error: failureMessage, + }); }; /** @@ -198,6 +225,9 @@ function GameStateInnerProvider({ children, gameConfig }) { setGeneratedCode(""); setCurrentBlockCount(0); setCodeEditorContent(""); + setPhaseStartTime(Date.now()); + setAttemptCount(0); + setFailureCount(0); }; /** @@ -317,6 +347,8 @@ function GameStateInnerProvider({ children, gameConfig }) { setFailureMessage, isDebugMode, setIsDebugMode, + attemptCount, + failureCount, }} > {children} diff --git a/app/src/services/plausible.js b/app/src/services/plausible.js new file mode 100644 index 0000000..dd3837c --- /dev/null +++ b/app/src/services/plausible.js @@ -0,0 +1,41 @@ +const PLAUSIBLE_API = import.meta.env.VITE_PLAUSIBLE_API || 'http://localhost/api/event'; +const DOMAIN = import.meta.env.VITE_PLAUSIBLE_DOMAIN || 'myapp-dev'; + +const sendEvent = (payload) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + fetch(PLAUSIBLE_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: controller.signal, + }) + .catch((e) => console.error('Plausible error:', e)) + .finally(() => clearTimeout(timeout)); +}; + +export const trackPageView = (pathname) => { + if (!pathname) return; + + sendEvent({ + domain: DOMAIN, + url: `${window.location.origin}${pathname}`, + referrer: document.referrer, + screenWidth: window.innerWidth, + name: 'pageview', + }); +}; + +export const trackEvent = (eventName, properties = {}) => { + if (!eventName) return; + + sendEvent({ + domain: DOMAIN, + url: window.location.href, + referrer: document.referrer, + screenWidth: window.innerWidth, + name: eventName, + props: properties, + }); +}; diff --git a/app/vite.config.js b/app/vite.config.js index 4a4676d..aa23009 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -74,6 +74,9 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, + server: { + allowedHosts: ["localhost", "dev.local", "decoda.mtst.tec.br"], + }, plugins: [ react(), copyLetramentoAtividades(), diff --git a/docs/docs/plataforma/arquitetura/coleta-eventos.md b/docs/docs/plataforma/arquitetura/coleta-eventos.md new file mode 100644 index 0000000..6863b15 --- /dev/null +++ b/docs/docs/plataforma/arquitetura/coleta-eventos.md @@ -0,0 +1,166 @@ +--- +sidebar_position: 10 +title: "Coleta de Eventos (Analytics)" +--- + +# Coleta de Eventos com Plausible + +Sistema de rastreamento de eventos educacionais através do Plausible Community Edition, permitindo análise de padrões de aprendizagem sem invasão de privacidade. + +## Fluxo de coleta + +```mermaid +graph TD + A["Atividade de Programação
(ex: PuzzleGame)"] -->|Usuário executa código| B["GameStateContext
(finalizeWithSuccess/Failure)"] + + B -->|Dispara evento| C["trackEvent
plausible.js"] + + C -->|Lê variáveis de ambiente| D{Qual ambiente?} + + D -->|Development| E["VITE_PLAUSIBLE_API
http://localhost/api/event"] + D -->|Production| F["VITE_PLAUSIBLE_API
plausible.mtst.tec.br"] + + E --> G["Fetch com timeout 5s
(AbortController)"] + F --> G + + G -->|Success| H["Evento registrado
no Plausible"] + + G -->|Timeout/Error| I["console.error
App continua"] + + H --> J["Dashboard Plausible
Análise de métricas"] +``` + +## Componentes principais + +### 1. **GameStateContext** (`contexts/GameStateContext.jsx`) + +Gerencia o ciclo de execução das atividades e dispara eventos: + +```javascript +// Quando a atividade tem sucesso +const finalizeWithSuccess = () => { + trackEvent('Activity Success', { + activity: gameConfig.gameId, + phase: currentPhase, + blocks: currentBlockCount, + attempts: attemptCount, // Total de tentativas + failures: failureCount, // Total de falhas + durationSeconds: Math.round((Date.now() - phaseStartTime) / 1000) + }); +}; + +// Quando a atividade falha +const finalizeWithFailure = () => { + trackEvent('Activity Failure', { + activity: gameConfig.gameId, + phase: currentPhase, + blocks: currentBlockCount, + error: failureMessage + }); +}; +``` + +Também rastreia: +- **Tentativas**: `attemptCount` incrementado a cada execução +- **Falhas**: `failureCount` incrementado a cada erro +- **Tempo**: `phaseStartTime` registrado ao mudar de fase + +### 2. **Serviço Plausible** (`services/plausible.js`) + +Abstração para envio de eventos com timeout e tratamento de erro: + +```javascript +const sendEvent = (payload) => { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + fetch(PLAUSIBLE_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: controller.signal, + }) + .catch((e) => console.error('Plausible error:', e)) + .finally(() => clearTimeout(timeout)); +}; +``` + +**Características**: +- ✅ Timeout de 5 segundos (não bloqueia a aplicação) +- ✅ Requisição não-bloqueante (não-await) +- ✅ Tratamento de erro silencioso (log no console, app continua) +- ✅ Configurável via variáveis de ambiente + +### 3. **Rastreamento de Page View** (`App.jsx`) + +Dispara `trackPageView` quando a rota muda: + +```javascript +useEffect(() => { + trackPageView(location.pathname); +}, [location.pathname]); +``` + +Registra visitas às páginas principais da plataforma. + +## Estrutura de dados + +### Payload enviado ao Plausible + +```json +{ + "domain": "myapp-dev", // ou https://decoda.mtst.tec.br (prod) + "url": "http://localhost/#/atividades/programacao/puzzle", + "referrer": "http://localhost/#/atividades", + "screenWidth": 1920, + "name": "Activity Success", + "props": { + "activity": "programacao/puzzle", + "phase": "3", + "blocks": "5", + "attempts": "4", + "failures": "2", + "durationSeconds": "127" + } +} +``` + +## Configuração por ambiente + +| Variável | Development | Production | +|----------|-------------|-----------| +| `VITE_PLAUSIBLE_API` | `http://localhost/api/event` | Variável de ambiente (prod) | +| `VITE_PLAUSIBLE_DOMAIN` | `myapp-dev` | `https://decoda.mtst.tec.br` | +| **Arquivo** | `app/.env` | `app/.env.production` | + +## Tratamento de falhas + +Se o servidor de analytics não estiver disponível: + +1. ✅ **Timeout (5s)**: Requisição é automaticamente cancelada +2. ✅ **Erro de conexão**: Capturado pelo `.catch()` e logado no console +3. ✅ **Sem impacto**: A aplicação continua funcionando normalmente +4. ✅ **Usuário não vê nada**: Erro não é exibido na UI + +## Privacidade e LGPD + +O Plausible CE foi escolhido porque: + +- 🔒 **Sem cookies**: Não usa rastreamento persistente +- 🔒 **Sem IP**: Não armazena endereço IP completo +- 🔒 **Dados educacionais apenas**: Coleta apenas métricas de uso da aplicação +- 🔒 **Sem rastreamento cross-site**: Não segue usuários fora da aplicação +- 📜 **Conformidade LGPD**: Alinhado com legislação brasileira de proteção de dados +- 🔓 **Software livre**: Código aberto, auditável, hospedado localmente + +## Fluxo de dados + +``` +Atividade → GameStateContext → trackEvent() → plausible.js → + ↓ + Fetch com timeout + ↓ + Plausible API ← erro/timeout → console.error (não bloqueia) + ↓ + Dashboard Plausible → Análise pedagógica +``` diff --git a/docs/docs/releases/v1.0.0.md b/docs/docs/releases/v1.0.0.md index a706ca9..63985ab 100644 --- a/docs/docs/releases/v1.0.0.md +++ b/docs/docs/releases/v1.0.0.md @@ -5,7 +5,7 @@ title: "1.0.0 — Lançamento inicial" # 1.0.0 — Lançamento inicial -**Data de lançamento:** 07/07/2026 +**Data de lançamento:** 07/05/2026 Esta é a versão inicial de lançamento público da plataforma Decoda. Ela consolida o desenvolvimento da trilha de letramento digital, da trilha de programação visual por blocos e do laboratório de Python, além da infraestrutura de implantação e da documentação. diff --git a/docs/docs/releases/v1.1.0.md b/docs/docs/releases/v1.1.0.md index 4bef4d8..2700c3b 100644 --- a/docs/docs/releases/v1.1.0.md +++ b/docs/docs/releases/v1.1.0.md @@ -5,7 +5,7 @@ title: "1.1.0" # 1.1.0 -**Data de lançamento:** 14/07/2026 +**Data de lançamento:** 14/05/2026 --- diff --git a/docs/docs/releases/v1.2.0.md b/docs/docs/releases/v1.2.0.md new file mode 100644 index 0000000..d24257b --- /dev/null +++ b/docs/docs/releases/v1.2.0.md @@ -0,0 +1,72 @@ +--- +sidebar_position: 1 +title: "1.2.0" +--- + +# 1.2.0 + +**Data de lançamento:** 13/06/2026 + +--- + +## Adicionado + +### Analytics com Plausible Community Edition + +Implementação de rastreamento de eventos e métricas de uso da plataforma através do [Plausible Community Edition](https://plausible.io/). Os dados coletados permitem entender como os estudantes interagem com as atividades, identificar gargalos de aprendizagem e melhorar continuamente a experiência educacional. + +#### Por que Plausible CE? + +A escolha pelo Plausible Community Edition é motivada por: + +- **Sem cookies**: Plausible não usa cookies de rastreamento, mantendo a privacidade dos usuários +- **Sem coleta invasiva**: Coleta apenas dados vinculados ao uso da aplicação — não rastreia histórico de navegação, dados pessoais ou comportamento fora do contexto educacional +- **Conformidade com LGPD**: Alinhado com as regulamentações brasileiras de proteção de dados +- **Software livre**: Pode ser hospedado localmente (Community Edition), oferecendo transparência e controle total + +#### Eventos rastreados nas atividades de programação + +Cada atividade de programação rastreia os seguintes eventos de aprendizagem: + +| Evento | Descrição | Propriedades | +|---|---|---| +| **Activity Success** | Estudante completou uma fase com sucesso | `activity`, `phase`, `blocks`, `attempts`, `failures`, `durationSeconds` | +| **Activity Failure** | Estudante encontrou um erro durante a execução | `activity`, `phase`, `blocks`, `error` | + +##### Detalhes das propriedades + +- **activity**: Identificador da atividade (ex: `programacao/puzzle`, `programacao/aspirador`) +- **phase**: Número da fase dentro da atividade +- **blocks**: Quantidade de blocos/linhas de código utilizados +- **attempts**: Número total de tentativas até o sucesso +- **failures**: Número de erros antes de conseguir sucesso +- **durationSeconds**: Tempo total gasto (em segundos) para completar a fase +- **error**: Mensagem de erro fornecida ao estudante (apenas em caso de falha) + +#### Análise pedagógica + +Essas métricas permitem: + +- Identificar fases com alta dificuldade (muitas tentativas/falhas antes do sucesso) +- Entender padrões de aprendizagem por atividade +- Medir o tempo médio de resolução de cada desafio +- Orientar decisões de redesign de atividades baseado em dados reais de uso + +--- + +## Técnico + +Arquivos de configuração: + +- `app/.env` (desenvolvimento) +- `app/.env.production` (produção) + +### Timeout de requisições + +Requisições para o Plausible possuem timeout de 5 segundos. Caso o servidor de analytics não responda no prazo, a requisição é automaticamente cancelada para não impactar a experiência do usuário. A aplicação funciona normalmente independente da disponibilidade do serviço de analytics. + +--- + +## Notas de migração + +Nenhuma ação necessária. O rastreamento é transparente para o usuário e não interfere na funcionalidade existente da plataforma.