
gibbon-replay
https://github.com/stephane-klein/gibbon-replay
- 7 septembre 2024 : Idée d'un outil de session recoding web minimaliste basé sur rrweb.
- 8 septembre 2024 : première version de gibbon-replay réalisé en 2h.
- 5 juin 2025 : liste d'issues pour gibbon-replay de juin 2025.
Journaux liées à cette note :
Liste d'issues pour gibbon-replay de juin 2025
Mon objectif dans cette note est de rassembler une liste d'issues que j'ai à l'esprit pour le projet gibbon-replay.
Dans cette note, les issues sont décrites en moins de 280 caractères, de manière approximative et sans doute un peu idiosyncrasique. Elles sont présentées dans un ordre quelconque.
- Dans le README, expliquer pourquoi j’ai créé ce projet et son ambition. Indiquer clairement que l’objectif est de rester simple à déployer (architecture monolithique) et que les utilisateurs plus ambitieux peuvent se tourner vers des solutions comme Posthog ou OpenReplay.
- Toujours dans le README, indiquer comme dans l'introduction de SilverBullet : « gibbon-replay is optimized for people with a hacker mindset ».
- [x] En tant qu'utilisateur, je peux visualiser l'espace mémoire total utilisé par l'ensemble des sessions. Issue GitHub : #4.
- [x] En tant qu'utilisateur, je peux visualiser l'espace mémoire consommé par chaque session individuellement.
- [x] En tant qu'utilisateur, je peux visualiser la durée de chaque session. Issue Github : #3.
- [x] En tant qu'utilisateur, je peux consulter, session par session, la présence ou non des actions utilisateur. Issue GitHub : #6.
- [ ] Optimiser la densité d'affichage de la liste des sessions en regroupant plusieurs données dans des cellules multilignes.
- En tant qu'utilisateur, dans la page liste des sessions, je peux appliquer un filtre sur les champs suivants : durée, taille mémoire ou mouvement de souris.
- En tant qu'utilisateur, dans la page détail d'une session, je peux visualiser les titres et les URLs des pages décrivant le parcours effectué par l'utilisateur.
- En tant qu'utilisateur, je peux visualiser un résumé textuel, du parcours utilisateur d'une session, rédigé par un agent conversationnel de petite taille.
- En tant qu'utilisateur avancé, je peux effectuer des recherches avancées sur le contenu des URLs présentes dans le parcours utilisateur. Par exemple, l'utilisateur peut saisir du code JavaScript qui permet de tester une condition sur toutes les URLs parcourues lors d'une session. Si la condition est positive, alors le résultat doit être sauvegardé dans un champ json de la session.
- En tant qu'utilisateur avancé, je peux rechercher des informations spécifiques dans le contenu des URLs présentes dans le parcours d'une session. Par exemple, je peux saisir un code JavaScript personnalisé pour tester une condition (comme la présence d'un
utm_source
oucampaign
) sur toutes les URLs parcourues. Si cette condition est vérifiée, les résultats correspondants sont stockés dans un champ json dans la session, permettant d'effectuer par la suite un filtre sur la liste des sessions. - User Story qui ressemble à la précédente : en tant qu'utilisateur avancé, je peux rechercher les balises HTML qui ont déclenché un événement "click" durant un parcours de session. Pour ce faire, il peut saisir du code JavaScript personnalisé pour tester une condition spécifique (comme la présence d'un attribut, d'une classe, etc.) sur ces balises. Les résultats de cette recherche sont enregistrés dans un champ JSON associé à la session, permettant d'effectuer par la suite un filtre sur la liste des sessions.
- En tant qu'utilisateur, je peux activer / désactiver l'envoi de notifications web sur des filtres de session, filtres avancés inclus.
- Permettre à une instance gibbon-replay d'enregistrer et de gérer plusieurs sites en même temps, en single-tenant.
- Ajouter un support multiutilisateurs — toujours en mode single-tenant. Permettre l'authentification par magic link et par username et password.
- Permettre la gestion des utilisateurs par API REST.
- Permettre de supprimer automatiquement des sessions en fonction de critères de filtres.
- En tant qu'utilisateur, je peux supprimer des sessions en mode batch.
Prochaine étape : créer ces issues plus détaillé dans : https://github.com/stephane-klein/gibbon-replay/issues
J'ai terminé poc-svelteki-web-notification
Je viens de terminer un POC nommé poc-sveltekit-web-notification
, qui m'a permis d'apprendre à implémenter la fonctionnalité Push API dans une PWA.
Quelques ressources qui m'ont été utiles :
- Push API
- Web Workers API
- Using VAPID with WebPush
- Cet exemple de MDN Web Docs :
push-subscription-management
.
Je n'ai aucune idée de pourquoi ce repository est archivé et par quoi il a été remplacé. - J'ai lu en partie Voluntary Application Server Identification (VAPID) for Web Push - RFC 8292
- SvelteKit - Service workers
Ma prochaine étape : intégrer cette fonctionnalité dans gibbon-replay.
J'ai découvert la fonctionnalité SvelteKit Shared hooks init
J'ai bien fait de partager poc-sveltekit-custom-server dans la section discussion GitHb de Sveltekit car cela m'a permis de découvrir via ce commentaire l'existence de la fonctionnalité native SvelteKit nommée Shared hooks init.
This function runs once, when the server is created or the app starts in the browser, and is a useful place to do asynchronous work such as initializing a database connection.
Cette fonctionnalité a été introduite dans la version 2.10.0 de SvelteKit publiée le 10 décembre 2024.
C'est particulièrement frustrant car j'ai cherché cette fonctionnalité à plusieurs reprises entre mi-2022 et mi-2024, sans la trouver. Je me souviens même avoir lu une issue de Rich Harris expliquant que cette fonctionnalité était complexe à implémenter.
Il y a quelques semaines, lors du développement de poc-sveltekit-custom-server
, j'ai refait une recherche de fonctionnalité "init", mais en me limitant à la documentation "Node servers". La présence de "Graceful shutdown" m'a paradoxalement induit en erreur : j'en ai déduit que s'il n'y avait pas d'équivalent pour l'initialisation sur cette page, c'est que la fonctionnalité n'existait toujours pas 😔.
Conséquence de tout cela :
- Je vais utiliser Shared hooks init dans gibbon-replay ;
- J'ai indiqué dans
poc-sveltekit-custom-server
que je recommande d'utiliser "Shared hooks init"
Bilan de poc-sveltekit-custom-server
Contexte et objectifs
Dans le projet gibbon-replay, j'ai besoin d'exécuter une tâche une fois par jour pour supprimer des anciennes sessions.
gibbon-replay utilise une base de données SQLite qui ne dispose pas nativement de fonctionnalité de type Time To Live, comme on peut trouver dans Clickhouse.
SQLite ne propose pas non plus d'équivalent à pg_cron — ce qui est tout à fait normal étant donnée que SQLite est une librairie et non pas un service à part entière.
Le projet gibbon-replay est un monolith (j'aime les monoliths !) et je souhaite conserver ce choix.
Face à ces contraintes, une solution consiste à intégrer une solution comme Cron for Node.js directement dans l'application gibbon-replay.
Je pense que je dois implémenter cela dans un SvelteKit Custom Server, ce qui me permettrait d'exécuter cette tâche de purge à intervalles réguliers tout en conservant l'architecture monolithique.
Il y a quelques jours, j'ai décidé de tester cette idée dans un POC nommé : poc-sveltekit-custom-server
.
J'ai aussi décidé d'expérimenter un objectif supplémentaire dans ce POC : lancer la migration du modèle de données dès le lancement du monolith et non plus lors de la première requête HTTP reçue par le service.
Enfin, je souhaitais ne pas dégrader l'expérience développeur (DX), c'est à dire, je souhaitais pouvoir continuer à simplement utiliser :
$ pnpm run dev
ou
$ pnpm run build
$ pnpm run preview
sans différence avec un projet SvelteKit "vanilla".
Résultats du POC et enseignements
Tout d'abord, le POC fonctionne parfaitement 🙂, sans dégrader l'expérience développeur (DX), qui ressemble à ceci :
$ mise install
$ pnpm install
$ pnpm run load-seed-data
Start data model migration…
Data model migration completed
Start load seed data...
seed data loaded
Lancement du projet en mode développement :
$ pnpm run dev
Start data model migration…
Data model migration completed
Server started on http://localhost:5173 in development mode
Lancement du projet "buildé" :
$ pnpm run build
$ pnpm run preview
Start data model migration…
Data model migration completed
Server started on http://localhost:3000 in production mode
Les migrations et les données "seed.sql" se trouvent dans le dossier /sqls/
.
Le SvelteKit Custom Server est implémenté dans le fichier src/server.js
et il ressemble à ceci :
import express from 'express';
import cron from 'node-cron';
import db, { migrate } from '@lib/server/db.js';
const isDev = process.env.ENV !== 'production';
migrate(); // Lancement de la migration du modèle de donnée dès de lancement du serveur
// Configuration d'une tâche exécuté toutes les heures
cron.schedule(
'0 * * * *',
async () => {
console.log('Start task...');
console.log(db().query('SELECT * FROM posts'));
console.log('Task executed');
}
);
async function createServer() {
const app = express();
...
Personnellement, je trouve cela simple et minimaliste.
Point de difficulté
SvelteKit utilise des "module alias", comme par exemple $lib
.
Problème, par défaut, ces "module alias" ne sont pas configurés lors de l'exécution de node src/server.js
.
Pour me permettre d'importer dans src/server.js
des modules de src/lib/server/*
comme :
import db, { migrate } from '@lib/server/db.js';
j'ai utilisé la librairie esm-module-alias
.
Ceci complexifie un peu le projet, j'ai dû configurer ceci dans /package.json
:
{
"scripts": {
"dev": "ENV=development node --loader esm-module-alias/loader --no-warnings src/server.js",
"preview": "ENV=production node --loader esm-module-alias/loader --no-warnings build/server.js",
...
"aliases": {
"@lib": "src/lib/"
}
}
- ajout de
--loader esm-module-alias/loader --no-warnings
- et la section
aliases
Et dans /vite.config.js
:
export default defineConfig({
plugins: [sveltekit()],
resolve: {
alias: {
'@lib': path.resolve('./src/lib')
}
}
});
- ajout de
alias
Le fichier src/server.js
contient du code spécifique en fonction de son contexte d'exécution ("dev" ou "buildé") :
if (isDev) {
const { createServer: createViteServer } = await import('vite');
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom'
});
app.use(vite.middlewares);
} else {
const { handler } = await import('./handler.js');
app.use(handler);
}
En mode "dev" il utilise Vite et en "buildé" il utilise le fichier build/handler.js
généré par SvelteKit build en mode SSR.
Le fichier src/server.js
est copié vers le dossier /build/
lors de l'exécution de pnpm run build
.
J'ai testé le bon fonctionnement du POC dans un container Docker.
J'ai intégré au projet un deployment-playground : https://github.com/stephane-klein/poc-sveltekit-custom-server/tree/main/deployment-playground.
La suite...
Je souhaite rédiger cette note en anglais et la publier sur https://github.com/sveltejs/kit/discussions et https://old.reddit.com/r/sveltejs/ afin :
- d'avoir des retours d'expérience
- de découvrir des méthodes alternatives
- et partager la méthode que j'ai utilisée, qui sera peut-être utile à d'autres développeurs Svelte 🙂
Update du 2025-05-29 à 00:07 - Je viens de publier ceci :
- https://github.com/sveltejs/kit/discussions/13841
- https://old.reddit.com/r/sveltejs/comments/1kxtz1u/custom_sveltekit_server_dev_production_with/?
2025-05-29 : voir J'ai découvert la fonctionnalité SvelteKit Shared hooks init
Journal du dimanche 20 octobre 2024 à 10:04
La version 5 de Svelte vient de sortir : 5.0.0.
Il y a un an, j'avais lu le billet Introducing runes. Depuis, j'ai suivi ce sujet de loin.
J'aimerais tester et apprendre à utiliser la fonctionnalité rune.
#JeMeDemande dans quel projet 🤔. Est-ce que je préfère refactorer vers rune le projet sklein-pkm-engine ou gibbon-replay 🤔. Je pense que ces deux projets utilisent trop peu de "reactive state".
Je souhaite prochainement débuter le projet que j'ai présenté dans 2023-10-28_2008. Je pense que ça serait une bonne occasion pour créer mon premier projet 100% TypeScript avec Svelte 5 avec Rune.
Journal du vendredi 04 octobre 2024 à 20:58
Je viens de mettre en œuvre better-sqlite3-helper ici dans le projet gibbon-replay, afin d'utiliser son système de migration de schemas.
L'expérience développeur (DX) est bonne, j'ai apprécié les fonctions helpers query
, queryFirstRow
, queryFirstCell
, insert
, etc.
Journal du mardi 24 septembre 2024 à 17:41
gibbon-replay n'enregistre pas les sessions de Safari sous MacOS et iOS.
En analysant le problème, j'ai découvert le message d'erreur suivant :
Beacon API cannot load ... Reached maximum amount of queued data of 64Kb for keepalive requests
Je n'ai trouvé la limitation de 64Kb ni dans https://www.w3.org/TR/beacon/, ni dans https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon.
Cela semble être une spécificité de Safari.
#JaiDécouvert que posthog-js utilise fflate pour compresser les données. La compression a été ajoutée en décembre 2020, dans cette Merge Request : Compress events using fflate (gzip-js).
Ce que je compte faire pour corriger le bug que j'ai sous Safari :
- Si les données à envoyer ont une taille supérieure à 50ko, alors utiliser
fetch
sinon utilisersendBeacon
; - Utiliser fflate pour compresser les données.
Journal du samedi 21 septembre 2024 à 11:22
#JaiDécouvert better-sqlite3-migrate, #JeShouhaiteTester dans gibbon-replay.
#JaiDécouvert aussi better-sqlite3-helper qui propose un mécanisme de migration de base de données SQLite.
#JaiDécouvert aussi la méthode suivante, un peu plus "raw" : https://github.com/n1ru4l/character-overlay/blob/ed7b2e1a1f18982196b41fb544067db54cef433f/server/migrateDatabase.ts#L10
Journal du mardi 17 septembre 2024 à 17:07
Ici, dans le projet gibbon-replay, j'ai utilisé la librairie Temporal.
import { Temporal } from 'temporal-polyfill';
return Temporal.PlainDateTime
.from(value)
.toZonedDateTime('UTC')
.withTimeZone(
Intl.DateTimeFormat().resolvedOptions().timeZone
)
.toString({
offset: 'never',
timeZoneName: 'never'
})
.replace('T', ' ');
Le but est de convertir une donnée datetime UTC vers la timezone du navigateur.
Cependant, j'ai été déçu de constater qu'il n'existe pas de fonction de formatage intégrée similaire à celle de date-fns
pour formater une date/heure.
Je trouve dommage de devoir utiliser .replace('T', ' ');
pour supprimer le caractère T
dans la date formatée.