Fils de discussion

Des commentaires sur un feedback, stockés à côté de lui et servis par le même endpoint — avec le rôle équipe et les e-mails des relecteurs protégés par votre politique d'accès.

Un feedback porte un fil de commentaires : le relecteur demande « 16 ou 24 px ? », l'équipe répond, et la conversation reste à côté du retour au lieu de partir par e-mail. Les fils sont conservés par votre store et servis par @siteping/server sur l'endpoint que vous avez déjà monté — aucune nouvelle route.

Le widget montre le fil au relecteur et le laisse répondre ; le dashboard le montre à votre équipe, qui y répond et y supprime. La suite de cette page couvre le stockage et l'API HTTP.

Dans le widget

Le fil prolonge le message dans la vue détail d'un feedback, dans le panneau : du plus ancien au plus récent, avec un champ de réponse en dessous. Les réponses de l'équipe portent un badge Équipe.

  • Le champ de réponse n'apparaît que si la réponse peut être conservée : l'endpoint annonce capabilities.comments dans ses réponses de liste (en mode store : le store implémente addComment), et les permissions du feedback ne refusent pas les réponses. Face à un serveur antérieur aux fils — ni capabilities, ni comments — le widget n'affiche aucun fil, et rien ne casse.
  • Qui répond : le relecteur, résolu comme l'auteur d'un feedback — votre option identity, sinon celle enregistrée dans ce navigateur, sinon la fenêtre d'identification. La fermer n'envoie rien et ne signale rien ; le brouillon reste. Une réponse du widget est toujours enregistrée en client.
  • L'envoi : le bouton ou Ctrl/⌘+Entrée. Les pannes réseau et les 5xx sont réessayées comme pour un feedback, avec le même clientId : un réessai ne duplique jamais la réponse. Une réponse n'est jamais mise en file pour un chargement ultérieur : si elle échoue encore, le brouillon reste et le fil indique qu'elle n'est pas partie.
  • Votre code en est informé par onCommentAdded et l'événement comment:added (la charge utile : le commentaire enregistré, CommentResponse) ; un échec passe par onError et feedback:error, comme tout appel d'API.
  • Lire la réponse demande la liste : avec une apiKey, ajoutez "GET" à publicEndpoints — voir Endpoints publics.

Dans le dashboard

Le tiroir montre le fil du feedback ouvert, et laisse votre équipe répondre et supprimer une fois que vous avez dit à l'inbox qui écrit :

<SitepingInbox
  projects="my-project"
  endpoint="/api/siteping"
  apiKey={KEY}
  author={{ name: user.name, email: user.email }}
/>
  • author ({ name, email? }) est qui répond depuis cette inbox. Sans lui, les fils sont en lecture seule — et masqués quand ils sont vides. De même quand l'endpoint n'annonce pas de commentaires, ou quand les permissions du feedback refusent les réponses ; une réponse ne propose pas de suppression quand l'endpoint n'annonce pas de suppression, que les permissions du feedback la refusent, ou que l'inbox est en readOnly.
  • Les réponses demandent le rôle team, que le serveur ne garde que pour un appelant dont votre politique d'accès se porte garante — l'apiKey, ou canCommentAsTeam sous access (voir le rôle équipe). Toute autre réponse est enregistrée en client.
  • Rien n'est optimiste : une réponse s'affiche une fois enregistrée, et un échec garde le brouillon. Supprimer une réponse demande confirmation ; comme tout DELETE, il faut l'apiKey ou l'accord de votre politique d'accès.
  • Sans interface : useSitepingInbox renvoie canComment, canDeleteComment, addComment(id, body, clientId?) et deleteComment(id, commentId) — voir le hook. Une source personnalisée s'y branche avec addComment et removeComment.

Ce que contient un commentaire

ChampTypeNotes
idstringAttribué par le store
feedbackIdstringLe feedback dont le fil le contient
bodystringNettoyé des espaces en bordure, de 1 à 5000 caractères
authorNamestringDe 1 à 200 caractères
authorEmailstringUne adresse valide, ou "" quand l'auteur n'en a pas (un utilisateur du dashboard). Masqué comme celui du feedback — voir les e-mails
authorRole"client" | "team"client pour le relecteur sur le site, team pour le côté projet — voir le rôle équipe
createdAtchaîne ISOQuand le store l'a enregistré

Chaque feedback d'une réponse porte comments, du plus ancien au plus récent : [] quand le fil est vide, ou quand le store ne garde aucun commentaire.

  • Des envois idempotents. Un commentaire porte aussi un clientId que le client génère. Renvoyer le même clientId renvoie le commentaire enregistré au lieu de l'ajouter deux fois : une requête relancée ne duplique jamais une réponse. Comme celui du feedback, il n'est jamais renvoyé.
  • 100 commentaires par fil (MAX_COMMENTS_PER_FEEDBACK). Les listes embarquent les fils entiers, qui ne sont pas paginés : la limite borne le poids d'un feedback.
  • Un commentaire ne touche pas à l'updatedAt du feedback. updatedAt suit les champs propres au feedback (son statut) ; le createdAt du commentaire le plus récent dit quand le fil a bougé pour la dernière fois.
  • Supprimer un feedback supprime son fil.

API HTTP

Les commentaires partagent l'unique endpoint : un corps de POST avec un feedbackId est un commentaire, un corps de DELETE avec un commentId en supprime un. Les corps propres aux feedbacks ne portent jamais ces clés.

Poster un commentaire

POST /api/siteping
Content-Type: application/json

{
  "projectName": "mon-site",
  "feedbackId": "cm3f8x…",
  "body": "C'est 16 ou 24 px ?",
  "authorName": "Alice",
  "authorEmail": "alice@client.example",
  "clientId": "4f0c2b8e-4c1d-4f7a-9d4e-6b1f2a3c5d7e"
}

Il répond 201 avec le commentaire. authorRole est optionnel et vaut client par défaut. clientId est obligatoire — [a-zA-Z0-9_-], jusqu'à 200 caractères : générez-en un par commentaire (crypto.randomUUID()) et renvoyez le même en cas de relance.

Supprimer un commentaire

DELETE /api/siteping
Content-Type: application/json

{ "projectName": "mon-site", "feedbackId": "cm3f8x…", "commentId": "cm4a1k…" }

Il répond 200 { "deleted": true }. Comme tout DELETE, il exige l'apiKey — ou ce que demande votre politique d'accès.

Lire les fils

La liste GET ne change pas, à deux ajouts près : les comments de chaque feedback, et un objet capabilities qui dit si le store accepte les commentaires (comments) et sait les supprimer (deleteComments) — un client peut ainsi masquer sa zone de réponse, ou ses boutons de suppression, d'emblée au lieu de tomber sur un 501 :

{
  "feedbacks": [{ "id": "cm3f8x…", "message": "…", "comments": [] }],
  "total": 1,
  "capabilities": { "comments": true, "deleteComments": true }
}

Erreurs

StatutQuand
400 { errors }Le corps échoue à la validation (texte vide, e-mail invalide, clientId absent…)
401 / 403Identifiants absents, ou refus d'authorize ou d'un contrôle CSRF
404 { error: "Feedback not found" }Feedback inconnu, ou d'un autre projet
404 { error: "Comment not found" }Le commentaire n'est pas dans le fil de ce feedback
409 { error }Le fil contient déjà 100 commentaires, ou le clientId est déjà utilisé sur un autre feedback
501 { error: "Comments are not supported by this store" }Le store ne garde aucun commentaire — voir activer les fils

Le rôle équipe

Le widget poste depuis des navigateurs anonymes : l'authorRole qu'envoie une requête n'est donc qu'une prétention. Le handler ne garde team que lorsque votre politique d'accès se porte garante de l'appelant, et marque tout autre commentaire client :

  • Avec la politique apiKey, la requête doit porter Authorization: Bearer <apiKey>. Le POST public, une mauvaise clé et un handler sans apiKey obtiennent tous client. Un widget configuré avec apiKey envoie cette clé depuis le navigateur de chaque visiteur, qui pourrait alors poster au nom de l'équipe : gardez apiKey hors des widgets publics.
  • Avec access, c'est canCommentAsTeam(principal) qui décide. Sans lui, c'est votre réponse à canReadAuthorEmail — qui peut lire les e-mails des relecteurs est du côté projet —, donc une politique qui distingue déjà les visiteurs n'a rien à ajouter. Une politique sans aucun des deux marque chaque commentaire client : écrire en tant qu'équipe permet de parler au nom de votre agence dans le fil, ce droit n'est donc jamais accordé par défaut.

authorize voit deux actions de plus : createComment et deleteComment, avec le feedbackId — et le commentId pour une suppression. Lors d'un dryRun, qui remplit les permissions du feedback, deleteComment arrive sans commentId et sa réponse vaut pour tout le fil : protégez une règle par auteur avec if (!commentId) return false, qui masque les boutons de suppression tandis que le vrai DELETE passe toujours par votre règle.

createSitepingHandler({
  store,
  access: {
    authenticate: (request) => getSessionUser(request),
    authorize: ({ principal, action }) =>
      action === "deleteComment" ? principal.isStaff : true,
    canReadAuthorEmail: (principal) => principal.isStaff,
    // Optionnel ici : il reprendrait canReadAuthorEmail.
    canCommentAsTeam: (principal) => principal.isStaff,
  },
});

E-mails des relecteurs

L'authorEmail d'un commentaire est masqué exactement comme celui du feedback : vide sauf si le demandeur peut le lire — l'apiKey en Bearer, ou canReadAuthorEmail avec access. La réponse à un POST de commentaire renvoie à son auteur l'e-mail du nouveau commentaire, comme le fait un POST de feedback ; un POST de feedback rejoué ne renvoie que l'e-mail de son auteur, jamais ceux du fil.

Endpoints publics et abus

  • Lire les réponses depuis le widget. Avec une apiKey, publicEndpoints vaut par défaut ["POST", "OPTIONS"] : GET exige la clé, donc un widget public peut poster un commentaire mais pas lire la réponse. Ajoutez "GET" à publicEndpoints pour que les relecteurs lisent leurs fils — les e-mails restent masqués.
  • Limitation de débit. Le POST public accepte aussi les commentaires : quiconque connaît l'id d'un feedback et le nom de votre projet peut en ajouter. Limitez le débit de POST dans votre framework ou votre reverse proxy ; la limite de 100 commentaires borne la taille d'un fil, pas sa vitesse de remplissage.
  • Notifications. Les webhooks et onCreated se déclenchent pour les nouveaux feedbacks seulement, pas pour les commentaires.

Activer les fils sur votre store

StoreQuoi faire
PrismaLancez npx @siteping/cli sync pour ajouter le modèle SitepingComment, puis npx prisma db push (ou npx prisma migrate dev). Tant que le client n'est pas généré avec lui, les fils se lisent vides et les écritures de commentaires répondent 501
DrizzleExportez sitepingComments depuis votre schéma à côté des autres tables, puis générez et appliquez la migration avec drizzle-kit
Memory, localStorageRien — les deux gardent les fils. Les données localStorage enregistrées par une version antérieure se lisent avec des fils vides
Le vôtreImplémentez les méthodes optionnelles addComment et deleteComment — createCollectionStore fournit les deux
Modifier sur GitHub

Sur cette page