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.