Sommaire de la page
À quoi ça sert
Glance lit déjà vos statistiques d'audience dans Umami ou Plausible. Mais certains chiffres ne s'y trouvent pas, parce qu'ils vivent dans votre base de données : la rétention de vos joueurs, le nombre de comptes créés, les installations d'une application, les abonnements actifs…
Une source de métriques est une petite API que vous ajoutez à votre site. Elle expose deux adresses en lecture seule : un catalogue (la liste de vos indicateurs) et des séries (leurs valeurs jour par jour). Glance s'y connecte avec un jeton, sans rien savoir de votre base : tout le calcul reste chez vous.
Chaque indicateur devient une carte « Métrique du site » dans l'onglet Tableaux du site, avec sa courbe, son chiffre global et sa comparaison à la période choisie, comme n'importe quelle autre carte.
Deux cartes d'exemple : un taux avec sa courbe, puis un compteur découpé par plateforme.
Ce que vous devez fournir est volontairement petit : deux routes GET, du JSON, un jeton. La suite de cette page décrit le contrat exact (version 1), la façon dont Glance interprète vos réponses, des exemples, et un serveur minimal que vous pouvez lancer en une minute.
Brancher la source dans l'app
Une source se configure par site. Prévoyez l'adresse de base de votre API et un jeton.
- Ouvrez le site concerné dans Glance, puis son onglet Tableaux.
- Tout en bas, sélectionnez « Source de métriques » (le même lien apparaît dans l'état vide, quand il n'y a encore aucun tableau).
- Saisissez l'adresse de l'API : l'URL de base, celle qui précède
catalogueetseries. Lehttps://et le slash final sont facultatifs, Glance les ajoute. - Saisissez le jeton. Il est gardé dans le trousseau de l'appareil (Keychain), jamais dans les réglages.
- Sélectionnez Tester : Glance appelle
catalogueet annonce « 3 indicateurs disponibles », ou le message d'erreur du serveur (voir Erreurs). - Sélectionnez Enregistrer.
- De retour dans l'onglet Tableaux, sélectionnez « Nouveau tableau », choisissez le genre « Métrique du site », puis un indicateur du catalogue (ou une famille, voir Familles). La carte apparaît et suit la période affichée.
Bon à savoir. L'URL et le jeton voyagent avec la synchronisation chiffrée de bout en bout entre vos appareils, mais pas avec le QR de transfert. « Retirer la source » supprime aussi le jeton et les cartes « Métrique » du site, qui n'auraient plus rien à afficher.
Le contrat, version 1
Deux routes, relatives à l'adresse de base saisie dans l'app. Tout est du JSON, en UTF-8.
Authentification
Chaque requête, catalogue comme series, porte l'en-tête Authorization: Bearer <jeton>. Le jeton est un secret que vous choisissez et que votre serveur compare : Glance ne le décode pas. Servez l'API en HTTPS.
GET https://api.exemple.fr/metriques/catalogue
Authorization: Bearer <votre jeton>
Accept: application/json
GET catalogue
Sans paramètre. Renvoie la liste des indicateurs que Glance peut proposer.
{
"version": 1,
"indicateurs": [
{
"id": "retention_j1",
"libelle": "Rétention J1",
"description": "Part des nouveaux joueurs revenus le lendemain.",
"unite": "pourcentage",
"granularites": ["jour", "mois"],
"delai_jours": 1,
"famille": "retention",
"libelle_famille": "Rétention",
"donnees_jusqu_au": "2026-10-18"
}
]
}
| Champ | Type | Statut | Rôle |
|---|---|---|---|
version | nombre | requis | Toujours 1. Toute autre valeur est refusée. |
indicateurs | tableau | requis | Les indicateurs, dans l'ordre où Glance les présente. |
id | texte | requis | Identifiant stable, repris dans ids de series. Ne le changez pas : les cartes déjà créées le mémorisent. |
libelle | texte | requis | Nom affiché sur la carte et dans la liste. |
description | texte | facultatif | Phrase d'aide affichée dans la liste de choix. |
unite | texte | recommandé | pourcentage (valeurs de 0 à 100) ou nombre. Absente ou inconnue : traitée comme nombre. |
granularites | tableau de textes | facultatif | Seule la valeur "heure" est lue : elle autorise les points horaires (voir Heures). jour et mois sont demandés selon la période affichée, annoncés ou non. |
delai_jours | entier | facultatif | Jours à attendre après un jour J pour que sa cohorte soit mesurable (J1 : 1, J7 : 7). Absent ou 0 pour un nombre. Voir Délais. |
famille | texte | facultatif | Identifiant d'un groupe d'indicateurs qui ne diffèrent que par leur délai. Voir Familles. |
libelle_famille | texte | facultatif | Nom de la famille (celui du premier membre qui en porte un ; sinon famille). |
donnees_jusqu_au | date | facultatif | Dernier jour publié par la source, AAAA-MM-JJ (tout autre format est ignoré). Voir Données en retard. |
segments | tableau | facultatif | Découpage d'un nombre en parts. Chaque entrée : id (requis), libelle (défaut : l'id), estime (booléen, vrai si la part n'est qu'une estimation : « · estimé » s'ajoute au libellé). Une entrée sans id est ignorée. Voir Segments. |
GET series
Renvoie les valeurs d'un ou plusieurs indicateurs sur une plage de dates.
GET https://api.exemple.fr/metriques/series?ids=retention_j1
&de=2026-10-12&a=2026-10-18&granularite=jour&tz=Europe/Paris
Authorization: Bearer <votre jeton>
| Paramètre | Valeur | Rôle |
|---|---|---|
ids | identifiants séparés par des virgules | Glance en demande un seul à la fois, mais votre serveur doit accepter la liste. |
de, a | AAAA-MM-JJ | Premier et dernier jour, inclusifs, dans le fuseau tz. 400 jours au plus : au-delà, répondez 400. |
granularite | jour, mois ou heure | La taille d'un point. heure n'est demandée que dans les conditions décrites plus bas. Glance ne demande jamais de semaine. |
tz | fuseau IANA | Celui de l'appareil (Europe/Paris), UTC s'il ne le donne pas. C'est dans ce fuseau que vous découpez les jours. |
{
"version": 1,
"series": [
{
"id": "retention_j1",
"points": [
{
"t": "2026-10-12",
"valeur": 14.3,
"numerateur": 21,
"denominateur": 147
}
]
}
]
}
| Champ | Type | Statut | Rôle |
|---|---|---|---|
version | nombre | requis | Toujours 1. |
series | tableau | requis | Une entrée par id demandé. Un id absent de la réponse donne une carte sans point. |
id | texte | requis | L'indicateur de la série. |
points | tableau | requis | Un point par bucket de la plage, vides compris. Le premier peut commencer avant de (le mois qui contient le premier jour, par exemple). |
t | texte | requis | Début du bucket, en heure locale sans fuseau : AAAA-MM-JJ (jour ; pour un mois, son premier jour), ou AAAA-MM-JJTHH:00 (heure). |
valeur | nombre ou null | requis | La mesure. null : non mesurable (voir Unités et trous). |
numerateur, denominateur | entiers | requis pour un % | Les effectifs derrière le pourcentage. Ils servent au taux global de la carte. Absents : lus comme 0. Ignorés pour un nombre. |
segments | objet | facultatif | Pour un indicateur découpé : la valeur de chaque part, { "site": 190, "appli": 230 }. Un point sans cette clé est inconnu, pas nul. |
totaux | objet | facultatif | Sur la série : le total distinct de chaque part sur toute la plage demandée. Voir Segments. |
Erreurs
Répondez avec un code HTTP, Glance traduit chacun en message qu'il affiche tel quel sur la carte (et sous le bouton Tester).
| Réponse du serveur | Message affiché dans Glance |
|---|---|
404 | Source non configurée sur le serveur |
401 (jeton absent ou faux) | Jeton refusé |
400 avec {"error":{"code":"…"}} | Requête refusée (code) ; sans code lisible : « Requête refusée (inconnu) » |
200 avec version ≠ 1 | Version d'API non prise en charge |
| Autre statut, ou corps qui n'est pas un objet JSON | Réponse inattendue du serveur |
| Pas de réponse (réseau, délai dépassé : 15 s pour se connecter, 25 s pour répondre) | Chargement impossible. |
{ "error": { "code": "plage_trop_longue" } }
Glance ne relance pas automatiquement après l'une de ces erreurs : le message reste sur la carte jusqu'au prochain rafraîchissement, pour que vous puissiez corriger.
Comment Glance lit vos réponses
Le contrat tient en quelques champs, mais chacun a une conséquence visible. Voici les règles, telles que l'app les applique.
Unités, trous et taux global
pourcentage:valeurva de 0 à 100 (14,3 pour 14,3 %). Glance la divise par 100 pour l'afficher.nombre:valeurest un compte. Unnully vaut 0 : un compteur n'est jamais « non mesurable ».valeur: nullsur un pourcentage = non mesurable (cohorte pas terminée, ou trop peu d'effectif). C'est un trou dans la courbe, jamais un 0 : « rien mesuré » n'est pas « 0 % ». Ses effectifs ne comptent pas dans le total.- Seuil de confidentialité : 5. Renvoyez
nullquand le dénominateur d'un bucket est inférieur à 5 : un taux sur trois personnes ne veut rien dire. Glance affiche lavaleurreçue sans la recalculer. - Taux global de la carte = Σ numérateurs ÷ Σ dénominateurs des points mesurés, jamais la moyenne des taux. Il n'est pas affiché si Σ dénominateurs est inférieur à 5.
- Les buckets que vous n'envoyez pas sont comblés : trou pour un pourcentage, 0 pour un nombre. Les points hors de la fenêtre affichée sont écartés.
Délais et cohortes (delai_jours)
La rétention J7 du jour J ne se connaît qu'au jour J + 7. Pour que la carte reste alignée sur la période choisie, Glance place chaque point à sa date de mesure, pas à sa date de cohorte.
- Votre
test le jour de la cohorte ;valeurest son taux, mesuré une foisdelai_joursécoulés. - Pour une période du 15 au 21 octobre, un indicateur J7 reçoit
de=2026-10-08&a=2026-10-14: les cohortes du 8 au 14, affichées aux dates du 15 au 21. - Un point dont le jour de mesure n'est pas terminé (aujourd'hui) reste un trou, quoi que réponde le serveur. Sur « Aujourd'hui », un J1 n'a donc rien de mesurable, et la carte dit « Pas encore mesurable sur cette période ».
- En mois, Glance demande des mois et décale chaque bucket de
delai_joursavant de le rattacher à son mois. Agrégez le mois entier côté serveur : chaque jour isolé tombe souvent sous le seuil de 5.
Familles (famille)
Plusieurs indicateurs qui ne diffèrent que par leur délai (J1, J7, J30) forment une famille. Dans l'éditeur, l'utilisateur choisit la famille une fois ; Glance choisit le membre à chaque calcul : le plus grand délai qui tient quatre fois dans la fenêtre affichée, sinon le plus court.
| Période | Durée | Membre retenu |
|---|---|---|
| Aujourd'hui, 24 h, 7 jours | 1 à 7 jours | J1 (J7 exigerait 28 jours) |
| 30 jours | 30 jours | J7 (28 ≤ 30 ; J30 exigerait 120) |
| 12 mois | 365 jours | J30 (120 ≤ 365) |
Une seule carte suit ainsi la période, au lieu de trois cartes dont deux sont vides. Elle affiche le nom de la famille et « J7 · s'ajuste à la période ». Exemple de catalogue plus bas, dans Exemples.
Les membres d'une famille doivent partager la même unite (Glance prend celle du plus court délai), et libelle_famille suffit sur un seul d'entre eux.
Données en retard (donnees_jusqu_au)
Certaines sources publient avec retard (les rapports d'une boutique d'applications, par exemple). Annoncez le dernier jour publié, AAAA-MM-JJ :
- Les buckets d'après sont des trous, même pour un
nombre: un jour pas encore publié n'est pas un jour à 0. Ils sont hors des totaux. - La carte note « Données jusqu'au 18 oct. ». Si toute la fenêtre est postérieure, elle dit « Pas encore publiées ».
- La date porte sur le
tde la série. Avec un délai, Glance la décale dedelai_jours, comme les points. - Côté serveur, il est conseillé de renvoyer pour le bucket mois qui contient cette date sa somme partielle, et
nullseulement pour un bucket qui commence après.
Segments (segments et totaux)
Un nombre peut être découpé en parts (site, appli, autre plateforme…). Déclarez-les dans le catalogue, puis donnez dans chaque point la valeur de chaque part.
- La carte trace une courbe « Total » plus une courbe par part (légende cliquable), puis une barre par part sur la période.
- Un point sans clé
segmentsest inconnu, pas zéro : la source ne savait pas encore découper ce jour-là. Chaque part y est un trou ;valeurreste le vrai total distinct. totauxdonne le nombre de distincts sur toute la plage pour chaque part. Les barres se calculent dessus, jamais sur la somme des points : un joueur actif deux jours compterait deux fois.- La somme des parts peut dépasser le total, si une même personne passe par deux plateformes : la barre se lit donc en part de Σ
totaux. - Tant qu'aucune part n'a de valeur, la carte reste un compteur simple. Les segments ne s'appliquent pas à un
pourcentage.
Heures (granularites)
Par défaut, même sur « Aujourd'hui », Glance demande des jours et la carte affiche un point par jour. Elle demande granularite=heure seulement si les trois conditions sont réunies :
- la période affichée se lit en heures (« Aujourd'hui », « 24 h »…) ;
- l'indicateur annonce
"heure"dansgranularites; - il n'a pas de délai (
delai_joursabsent ou 0).
Alors : de et a restent des jours (la source rend des journées entières, Glance ne garde que les heures de la fenêtre) ; t vaut AAAA-MM-JJTHH:00 en heure locale, un point par heure, vides compris (23 ou 25 points les jours de changement d'heure) ; 30 jours au plus, au-delà répondez 400 plage_trop_longue. Les totaux portent sur les journées entières.
Limites et bonnes pratiques
- 400 jours au plus par requête. Pour « Tout » ou une période perso plus longue, Glance ramène lui-même
deàa− 399 jours : les buckets d'avant restent vides (trou pour un %, 0 pour un nombre). - Version : toute
versionautre que1est refusée. Une évolution incompatible du contrat passera par une nouvelle version, pas par un changement silencieux. - Cache : mettez les réponses en cache côté serveur, 5 minutes conviennent. Plusieurs cartes et plusieurs appareils interrogent la même source.
- Lecture seule, sans donnée personnelle : n'exposez que des agrégats. Le seuil de 5 existe pour qu'aucun chiffre ne désigne une personne.
- Jeton : comparez-le à temps constant, révoquez-le si un appareil est perdu, et ne l'inscrivez jamais dans une URL.
Exemples de réponses
Un pourcentage, avec des trous
Rétention J1 sur sept jours. Le 14, la cohorte n'a que 3 personnes : null. Le 18, la cohorte n'est pas encore mesurable : null aussi. Le taux global est (21 + 17 + 23 + 18 + 15) ÷ (147 + 140 + 146 + 138 + 126), soit 94 ÷ 697 ≈ 13,5 %, sans les deux trous.
{
"version": 1,
"series": [{
"id": "retention_j1",
"points": [
{ "t": "2026-10-12", "valeur": 14.3, "numerateur": 21, "denominateur": 147 },
{ "t": "2026-10-13", "valeur": 12.1, "numerateur": 17, "denominateur": 140 },
{ "t": "2026-10-14", "valeur": null, "numerateur": 2, "denominateur": 3 },
{ "t": "2026-10-15", "valeur": 15.8, "numerateur": 23, "denominateur": 146 },
{ "t": "2026-10-16", "valeur": 13.0, "numerateur": 18, "denominateur": 138 },
{ "t": "2026-10-17", "valeur": 11.9, "numerateur": 15, "denominateur": 126 },
{ "t": "2026-10-18", "valeur": null, "numerateur": 0, "denominateur": 0 }
]
}]
}
Un nombre, avec des parts
Un indicateur découpé en trois plateformes, déclaré ainsi dans le catalogue :
{
"id": "joueurs_actifs",
"libelle": "Joueurs actifs",
"unite": "nombre",
"granularites": ["heure", "jour", "mois"],
"segments": [
{ "id": "site", "libelle": "Site" },
{ "id": "appli", "libelle": "Appli" },
{ "id": "tiers", "libelle": "Autres", "estime": true }
]
}
Sa série : le 18 n'a pas de clé segments (la journée n'est pas encore découpée), et totaux donne les distincts de la plage. La part « Site » vaut 310 ÷ (310 + 402 + 9) ≈ 43 %.
{
"version": 1,
"series": [{
"id": "joueurs_actifs",
"points": [
{ "t": "2026-10-16", "valeur": 412,
"segments": { "site": 190, "appli": 230, "tiers": 5 } },
{ "t": "2026-10-17", "valeur": 388,
"segments": { "site": 171, "appli": 221, "tiers": 4 } },
{ "t": "2026-10-18", "valeur": 120 }
],
"totaux": { "site": 310, "appli": 402, "tiers": 9 }
}]
}
Une famille J1 / J7 / J30
Trois indicateurs d'une même famille. Dans l'éditeur, une seule entrée « Rétention » apparaît.
{
"version": 1,
"indicateurs": [
{ "id": "retention_j1", "libelle": "Rétention J1", "unite": "pourcentage",
"granularites": ["jour", "mois"], "delai_jours": 1,
"famille": "retention", "libelle_famille": "Rétention" },
{ "id": "retention_j7", "libelle": "Rétention J7", "unite": "pourcentage",
"granularites": ["jour", "mois"], "delai_jours": 7,
"famille": "retention", "libelle_famille": "Rétention" },
{ "id": "retention_j30", "libelle": "Rétention J30", "unite": "pourcentage",
"granularites": ["jour", "mois"], "delai_jours": 30,
"famille": "retention", "libelle_famille": "Rétention" }
]
}
Des points horaires
Réponse à granularite=heure pour un indicateur sans délai qui annonce "heure" :
{
"version": 1,
"series": [{
"id": "joueurs_actifs",
"points": [
{ "t": "2026-10-18T00:00", "valeur": 12 },
{ "t": "2026-10-18T01:00", "valeur": 7 },
{ "t": "2026-10-18T02:00", "valeur": 3 }
]
}]
}
Un serveur minimal d'exemple
Environ soixante lignes de Node.js, sans dépendance : le module http natif suffit. Il sert un catalogue de deux indicateurs (comptes_crees, un nombre, et retention_j1, un pourcentage à délai), vérifie le jeton, valide les paramètres, gère jours et mois, applique le seuil de 5 et l'attente de la cohorte. Les chiffres sont fictifs : remplacez dayStats par vos requêtes.
const http = require('node:http');
const TOKEN = process.env.METRICS_TOKEN || 'change-me';
const DAY = 86_400_000;
const MAX_DAYS = 400;
const MIN_DEN = 5; // seuil de confidentialité : en dessous, valeur = null
const CATALOGUE = { version: 1, indicateurs: [
{ id: 'comptes_crees', libelle: 'Comptes créés', description: 'Inscriptions du jour.',
unite: 'nombre', granularites: ['jour', 'mois'] },
{ id: 'retention_j1', libelle: 'Rétention J1', description: 'Nouveaux revenus le lendemain.',
unite: 'pourcentage', granularites: ['jour', 'mois'], delai_jours: 1 },
] };
const DELAI = { comptes_crees: 0, retention_j1: 1 };
// À remplacer par vos requêtes : un jour -> { n, d } (numérateur, dénominateur).
const dayStats = (id, day) => {
const k = Date.parse(day) / DAY;
return id === 'comptes_crees' ? { n: 20 + (k % 7) * 3, d: 0 } : { n: 10 + (k % 5), d: 90 + (k % 9) };
};
function series(q) {
const ids = (q.get('ids') || '').split(',').filter(Boolean);
const de = Date.parse(q.get('de')), a = Date.parse(q.get('a'));
const gran = q.get('granularite') || 'jour';
if (!ids.length || ids.some((id) => !(id in DELAI))) return [400, 'indicateur_inconnu'];
if (isNaN(de) || isNaN(a) || a < de) return [400, 'dates_invalides'];
if ((a - de) / DAY + 1 > MAX_DAYS) return [400, 'plage_trop_longue'];
if (gran !== 'jour' && gran !== 'mois') return [400, 'granularite_inconnue'];
const today = Date.now() - (Date.now() % DAY);
return [200, { version: 1, series: ids.map((id) => {
const buckets = new Map();
for (let t = de; t <= a; t += DAY) {
const day = new Date(t).toISOString().slice(0, 10);
const key = gran === 'mois' ? day.slice(0, 8) + '01' : day;
const b = buckets.get(key) || { n: 0, d: 0, fini: true };
const s = dayStats(id, day);
buckets.set(key, { n: b.n + s.n, d: b.d + s.d, fini: b.fini && t + DELAI[id] * DAY < today });
}
return { id, points: [...buckets].map(([t, b]) => ({
t,
valeur: id === 'comptes_crees' ? b.n
: b.fini && b.d >= MIN_DEN ? Math.round((1000 * b.n) / b.d) / 10 : null,
numerateur: b.n, denominateur: b.d,
})) };
}) }];
}
http.createServer((req, res) => {
const url = new URL(req.url, 'http://x');
const send = (status, body) => {
res.writeHead(status, { 'content-type': 'application/json' });
res.end(JSON.stringify(status === 400 ? { error: { code: body } } : body));
};
if (req.headers.authorization !== `Bearer ${TOKEN}`) return send(401, {});
if (url.pathname === '/metriques/catalogue') return send(200, CATALOGUE);
if (url.pathname === '/metriques/series') return send(...series(url.searchParams));
send(404, {});
}).listen(3000, () => console.log('http://localhost:3000/metriques/'));
Lancez-le avec METRICS_TOKEN=mon-jeton node server.js, testez-le avec curl :
curl -H 'Authorization: Bearer mon-jeton' \
http://localhost:3000/metriques/catalogue
curl -H 'Authorization: Bearer mon-jeton' \
'http://localhost:3000/metriques/series?ids=retention_j1&de=2026-10-12&a=2026-10-18&granularite=jour'
Puis, dans Glance, saisissez l'adresse http://<ip-de-votre-machine>:3000/metriques/ et le jeton. En production, placez-le derrière HTTPS (un reverse proxy suffit) : le jeton circule dans chaque requête.
Ce n'est qu'un squelette. Il ne gère ni heure, ni delai_jours au-delà de la cohorte, ni segments, ni le cache. Ajoutez ce dont vos indicateurs ont besoin en suivant les sections précédentes.