Merge pull request 'feature/plausible' (#3) from feature/plausible into main
Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -73,13 +73,6 @@ web_modules/
|
|||||||
# Yarn Integrity file
|
# Yarn Integrity file
|
||||||
.yarn-integrity
|
.yarn-integrity
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
.cache
|
.cache
|
||||||
.parcel-cache
|
.parcel-cache
|
||||||
|
|||||||
2
app/.env
Normal file
2
app/.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_PLAUSIBLE_API=http://localhost/api/event
|
||||||
|
VITE_PLAUSIBLE_DOMAIN=myapp-dev
|
||||||
2
app/.env.production
Normal file
2
app/.env.production
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_PLAUSIBLE_API=https://plausible.mtst.tec.br/api/event
|
||||||
|
VITE_PLAUSIBLE_DOMAIN=https://decoda.mtst.tec.br
|
||||||
@@ -3,7 +3,7 @@ FROM node:20-alpine AS builder
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG GIT_COMMIT_HASH=unknown
|
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_APP_VERSION=$APP_VERSION
|
||||||
ENV VITE_GIT_HASH=$GIT_COMMIT_HASH
|
ENV VITE_GIT_HASH=$GIT_COMMIT_HASH
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/puzzle.svg" />
|
<link rel="icon" type="image/svg+xml" href="/puzzle.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<title>Decoda</title>
|
<title>Decoda</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "decoda",
|
"name": "decoda",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Aplicação educacional desenvolvida para ensino de programação básica e letramento digital",
|
"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",
|
"main": "main.cjs",
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
* @module App
|
* @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 { HashRouter as Router, Routes, Route, useLocation } from "react-router-dom";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import HomePage from "./pages/HomePage/HomePage";
|
import HomePage from "./pages/HomePage/HomePage";
|
||||||
import LabPython from "./pages/LabPython/LabPython";
|
import LabPython from "./pages/LabPython/LabPython";
|
||||||
import ScrollToTop from "./components/ScrollToTop";
|
import ScrollToTop from "./components/ScrollToTop";
|
||||||
|
import { trackPageView } from "./services/plausible";
|
||||||
|
|
||||||
const Playground = lazy(() => import("./pages/Playground/Playground"));
|
const Playground = lazy(() => import("./pages/Playground/Playground"));
|
||||||
const About = lazy(() => import("./pages/About/About"));
|
const About = lazy(() => import("./pages/About/About"));
|
||||||
@@ -68,6 +69,10 @@ function AppRoutes() {
|
|||||||
// keeps rendering behind the modal overlay.
|
// keeps rendering behind the modal overlay.
|
||||||
const backgroundLocation = location.state?.backgroundLocation;
|
const backgroundLocation = location.state?.backgroundLocation;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
trackPageView(location.pathname);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
|
|||||||
@@ -653,10 +653,6 @@ video {
|
|||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-0 {
|
|
||||||
bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-10 {
|
.bottom-10 {
|
||||||
bottom: 2.5rem;
|
bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
@@ -984,10 +980,6 @@ video {
|
|||||||
height: 0.25rem;
|
height: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-1\.5 {
|
|
||||||
height: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-10 {
|
.h-10 {
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
}
|
}
|
||||||
@@ -2885,10 +2877,6 @@ video {
|
|||||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1));
|
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 {
|
.bg-gray-300 {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
|
background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
|
||||||
@@ -7125,10 +7113,6 @@ video {
|
|||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration-1000 {
|
|
||||||
transition-duration: 1000ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.duration-150 {
|
.duration-150 {
|
||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
}
|
}
|
||||||
@@ -7149,10 +7133,6 @@ video {
|
|||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
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\] {
|
||||||
-webkit-text-stroke: 2px black;
|
-webkit-text-stroke: 2px black;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import React, {
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { gameEventBus } from "../utils/gameEvents";
|
import { gameEventBus } from "../utils/gameEvents";
|
||||||
import { GameProgressProvider, useGameProgress } from "./GameProgressContext";
|
import { GameProgressProvider, useGameProgress } from "./GameProgressContext";
|
||||||
|
import { trackEvent } from "../services/plausible";
|
||||||
|
|
||||||
export const GAME_STATES = {
|
export const GAME_STATES = {
|
||||||
PARADO: "parado",
|
PARADO: "parado",
|
||||||
@@ -89,6 +90,9 @@ function GameStateInnerProvider({ children, gameConfig }) {
|
|||||||
);
|
);
|
||||||
return urlParams.get(debugKey)?.toLowerCase() === "true";
|
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 getCodeFromWorkspace = useRef(null);
|
||||||
const getCodeFromEditor = 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
|
* @throws {console.error} Se editor não registrou a função de execução
|
||||||
*/
|
*/
|
||||||
const execute = () => {
|
const execute = () => {
|
||||||
|
setAttemptCount(prev => prev + 1);
|
||||||
|
|
||||||
if (editorType === "code") {
|
if (editorType === "code") {
|
||||||
if (getCodeFromEditor.current) {
|
if (getCodeFromEditor.current) {
|
||||||
const codigo = getCodeFromEditor.current();
|
const codigo = getCodeFromEditor.current();
|
||||||
@@ -145,6 +151,19 @@ function GameStateInnerProvider({ children, gameConfig }) {
|
|||||||
if (!completedPhases.includes(currentPhase)) {
|
if (!completedPhases.includes(currentPhase)) {
|
||||||
setCompletedPhases([...completedPhases, 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 = () => {
|
const finalizeWithFailure = () => {
|
||||||
setExecutionState(GAME_STATES.FALHA);
|
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("");
|
setGeneratedCode("");
|
||||||
setCurrentBlockCount(0);
|
setCurrentBlockCount(0);
|
||||||
setCodeEditorContent("");
|
setCodeEditorContent("");
|
||||||
|
setPhaseStartTime(Date.now());
|
||||||
|
setAttemptCount(0);
|
||||||
|
setFailureCount(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -317,6 +347,8 @@ function GameStateInnerProvider({ children, gameConfig }) {
|
|||||||
setFailureMessage,
|
setFailureMessage,
|
||||||
isDebugMode,
|
isDebugMode,
|
||||||
setIsDebugMode,
|
setIsDebugMode,
|
||||||
|
attemptCount,
|
||||||
|
failureCount,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
41
app/src/services/plausible.js
Normal file
41
app/src/services/plausible.js
Normal file
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -74,6 +74,9 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
allowedHosts: ["localhost", "dev.local", "decoda.mtst.tec.br"],
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
copyLetramentoAtividades(),
|
copyLetramentoAtividades(),
|
||||||
|
|||||||
166
docs/docs/plataforma/arquitetura/coleta-eventos.md
Normal file
166
docs/docs/plataforma/arquitetura/coleta-eventos.md
Normal file
@@ -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<br/>(ex: PuzzleGame)"] -->|Usuário executa código| B["GameStateContext<br/>(finalizeWithSuccess/Failure)"]
|
||||||
|
|
||||||
|
B -->|Dispara evento| C["trackEvent<br/>plausible.js"]
|
||||||
|
|
||||||
|
C -->|Lê variáveis de ambiente| D{Qual ambiente?}
|
||||||
|
|
||||||
|
D -->|Development| E["VITE_PLAUSIBLE_API<br/>http://localhost/api/event"]
|
||||||
|
D -->|Production| F["VITE_PLAUSIBLE_API<br/>plausible.mtst.tec.br"]
|
||||||
|
|
||||||
|
E --> G["Fetch com timeout 5s<br/>(AbortController)"]
|
||||||
|
F --> G
|
||||||
|
|
||||||
|
G -->|Success| H["Evento registrado<br/>no Plausible"]
|
||||||
|
|
||||||
|
G -->|Timeout/Error| I["console.error<br/>App continua"]
|
||||||
|
|
||||||
|
H --> J["Dashboard Plausible<br/>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
|
||||||
|
```
|
||||||
@@ -5,7 +5,7 @@ title: "1.0.0 — Lançamento inicial"
|
|||||||
|
|
||||||
# 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ title: "1.1.0"
|
|||||||
|
|
||||||
# 1.1.0
|
# 1.1.0
|
||||||
|
|
||||||
**Data de lançamento:** 14/07/2026
|
**Data de lançamento:** 14/05/2026
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
72
docs/docs/releases/v1.2.0.md
Normal file
72
docs/docs/releases/v1.2.0.md
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user