Vie privée du sondage
Le sondage « Pour de vrai, toi ? » pose une question de santé : le TDAH. Voici exactement ce qui se passe quand tu réponds. Pas de promesse en l’air : les extraits de code ci-dessous sont importés du code qui tourne en production.
En une phrase
Ta réponse traverse Cloudflare puis notre serveur, qui la transforme aussitôt en +1 dans un compteur. Rien de ce qu’on stocke ne contient d’identifiant.
Ce qui part de ton navigateur
Une seule requête POST /api/answer, avec ce JSON (par exemple) :
corps de la requête{
"version": 2,
"consent": true,
"lang": "fr",
"problem": "moderately",
"adhd": "suspected",
"tech": true,
"usage": "build",
"confide": "days_0",
"scoreBand": "mid",
"workAi": true,
"fatigue": {
"parallel": "two_three",
"fatigueNow": "high",
"fatigueChange": "higher",
"fog": "moderately"
}
}S’y ajoute un en-tête X-Turnstile-Token : un jeton anti-robots, à usage unique. Aucun cookie. Le prénom écrit sur le badge ne part pas, et ton score exact au test non plus : seulement sa tranche.
Ce que le serveur en fait
Il vérifie la taille, les valeurs et ton consentement, contrôle le jeton auprès de Turnstile sans lui transmettre ton IP, puis ajoute +1 à une ligne. C’est tout :
src/survey.tsexport async function handleAnswer(request: Request, env: SurveyEnv): Promise<Response> {
if (request.method !== 'POST') return empty(405, { Allow: 'POST' });
if (!isAllowedOrigin(request.headers.get('Origin'))) return empty(403);
if (!request.headers.get('Content-Type')?.startsWith('application/json')) return empty(415);
// 1. Corps borné à 1 Ko, puis validation stricte : clés et valeurs attendues seulement.
const body = await readBounded(request, MAX_BODY_BYTES);
if (body === null) return empty(413);
let answer: Answer | null = null;
try {
answer = parseAnswer(JSON.parse(body));
} catch {
answer = null;
}
if (!answer) return empty(400);
// 2. Anti-robots (Turnstile), puis limite globale de débit, sans IP.
if (!(await isHuman(request.headers.get('X-Turnstile-Token'), env.TURNSTILE_SECRET))) return empty(403);
const { success } = await env.ANSWER_LIMITER.limit({ key: 'answer' });
if (!success) return empty(429);
// 3. Une réponse = +1 sur une ligne du tronc et, si l'IA a servi au travail, +1 sur
// 8 paires du bloc fatigue. Ni IP, ni identifiant, ni heure : seulement le mois.
// Le batch D1 est atomique : tout ou rien.
const month = new Date().toISOString().slice(0, 7);
const statements = [
env.DB.prepare(
`INSERT INTO answers_v2 (version, month, lang, problem, adhd, tech, usage, confide, work_ai, score_band, n)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 1)
ON CONFLICT (version, month, lang, problem, adhd, tech, usage, confide, work_ai, score_band)
DO UPDATE SET n = n + 1`,
).bind(
answer.version,
month,
answer.lang,
answer.problem,
answer.adhd,
answer.tech ? 1 : 0,
answer.usage,
answer.confide,
answer.workAi ? 1 : 0,
answer.scoreBand,
),
];
if (answer.fatigue) {
const values: Record<string, string> = { ...answer.fatigue, adhd: answer.adhd, usage: answer.usage, problem: answer.problem };
for (const [a, b] of FATIGUE_PAIRS) {
statements.push(
env.DB.prepare(
`INSERT INTO fatigue_pairs (version, month, a_key, a_value, b_key, b_value, n)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1)
ON CONFLICT (version, month, a_key, a_value, b_key, b_value) DO UPDATE SET n = n + 1`,
).bind(answer.version, month, a, values[a]!, b, values[b]!),
);
}
}
try {
await env.DB.batch(statements);
} catch {
// Pas de journalisation : le message d'erreur pourrait contenir les valeurs liées.
return empty(503);
}
return empty(204, { 'Cache-Control': 'no-store' });
}Ce qu’on stocke
Voici les tables du questionnaire actuel (la table counters de la première version, plus courte, n’a pas de colonne en plus). Le tronc commun est un compteur par combinaison de réponses. Les réponses sur la fatigue, elles, ne sont jamais stockées ensemble : chaque envoi ajoute +1 à huit paires, par exemple « 4 tâches ou plus × fatigue en hausse » ou « TDAH × brouillard ».
migrations/0002_v2.sql-- Tronc commun : un compteur par combinaison de réponses.
CREATE TABLE answers_v2 (
version INTEGER NOT NULL, -- 2
month TEXT NOT NULL, -- 'YYYY-MM', calculé par le serveur
lang TEXT NOT NULL, -- 'fr' | 'en'
problem TEXT NOT NULL, -- 7 derniers jours : not_at_all | a_little | moderately | a_lot | extremely
adhd TEXT NOT NULL, -- diagnosed | suspected | no | undisclosed
tech INTEGER NOT NULL, -- 0 | 1
usage TEXT NOT NULL, -- usage le plus long : build | research | support | other | mixed
confide TEXT NOT NULL, -- jours de confidences sur 7 : days_0 | days_1_2 | days_3_7 | undisclosed
work_ai INTEGER NOT NULL, -- IA utilisée pour travailler ces 7 derniers jours : 0 | 1
score_band TEXT NOT NULL, -- score au test parodique : low | mid | high
n INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (version, month, lang, problem, adhd, tech, usage, confide, work_ai, score_band)
) WITHOUT ROWID;
-- Bloc fatigue : seulement des paires de réponses, jamais la combinaison complète.
CREATE TABLE fatigue_pairs (
version INTEGER NOT NULL, -- 2
month TEXT NOT NULL, -- 'YYYY-MM'
a_key TEXT NOT NULL, -- ex. 'parallel'
a_value TEXT NOT NULL, -- ex. 'four_plus'
b_key TEXT NOT NULL, -- ex. 'fatigueChange'
b_value TEXT NOT NULL, -- ex. 'higher'
n INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (version, month, a_key, a_value, b_key, b_value)
) WITHOUT ROWID;Ce qu’on ne fait pas
- Pas d’IP, de cookie, d’identifiant ni d’heure dans la base : seulement le mois.
- Les journaux du serveur sont coupés, pour les requêtes comme pour les erreurs :
wrangler.jsonc "observability": { "enabled": false }- Pas de mesure d’audience, pas de script tiers en dehors de Turnstile, chargé seulement quand tu commences à répondre.
Les limites, honnêtement
- Cloudflare, notre hébergeur, voit ton IP pour acheminer la requête et faire tourner Turnstile, comme pour toute page web.
- Une case qui ne compte qu’une réponse correspond aux réponses d’une seule personne. Sans identifiant, elle ne dit pas qui. Mais on ne prétend pas à un anonymat absolu.
- La base garde un historique de 30 jours (D1 Time Travel). En comparant deux états, un administrateur pourrait voir quelle case a bougé, à la minute près, sans savoir qui a répondu.
- Les chiffres viennent de réponses volontaires de visiteurs d’un site parodique : ce n’est pas un échantillon représentatif, et ça ne remplace pas une étude. La question « depuis que tu utilises l’IA, ta fatigue a… » mesure un ressenti, qui dépend de la mémoire, pas un effet de l’IA.
Base légale et droits
- Responsable : cmdAI (voir les mentions légales). Sous-traitant : Cloudflare (hébergement, base D1 en juridiction européenne, Turnstile).
- Base légale : ton consentement explicite (articles 6.1.a et 9.2.a du RGPD), donné en cochant la case. Sans cette case, le serveur refuse la réponse.
- Durée : les compteurs sont gardés tant que le projet existe. Aucune donnée individuelle n’est conservée.
- Tes droits : une fois ta réponse fondue dans les compteurs, on ne peut plus la retrouver ni la supprimer (article 11 du RGPD). C’est le prix de l’absence d’identifiant. Tu peux simplement ne pas répondre, nous écrire àcontact@cmdai.fr, ou saisir la CNIL.
Publication
Rien n’est publié pour l’instant. Quand on aura assez de réponses, on ne publiera que des groupes d’au moins 30 réponses, en pourcentages arrondis.
Le code
Le dépôt du site sera rendu public. En attendant, les extraits de cette page sont importés au moment du build depuis le code déployé.