Adapter Prisma
L'adapter de production — des handlers HTTP adossés à votre base, avec auth, CORS, redaction et webhooks.
@siteping/adapter-prisma transforme une URL de votre app en API de feedback complète. Il gère Prisma Client v5, v6 et v7, et exige Node 20+.
npm i @siteping/adapter-prismaLe montage
La factory renvoie un handler standard du Web (Request → Response) par méthode HTTP — les monter est le travail de votre framework :
// app/api/siteping/route.ts — Next.js App Router
import { createSitepingHandler } from "@siteping/adapter-prisma";
import { prisma } from "@/lib/prisma";
export const { GET, POST, PATCH, DELETE, OPTIONS } = createSitepingHandler({ prisma });C'est le handler de @siteping/server avec un store Prisma intégré : même surface HTTP, même modèle de sécurité et mêmes recettes de montage pour Hono, Express et les autres frameworks.
Options
| Option | Type | Défaut | Ce que ça fait |
|---|---|---|---|
prisma | SitepingPrismaClient | — | Votre client Prisma. Obligatoire sauf si vous passez store |
store | SitepingStore | — | Utiliser n'importe quel store au lieu de Prisma (prioritaire) |
screenshotStorage | ScreenshotStorage | — | Envoyer les captures quelque part de réel au lieu de les inliner en URL data:. Ignoré quand vous passez un store personnalisé |
caseInsensitiveSearch | boolean | détecté automatiquement | Détecté depuis le provider Prisma (actif pour PostgreSQL, MongoDB, CockroachDB). Ne s'applique que si prisma est fourni |
Toutes les options de @siteping/server fonctionnent aussi — apiKey, allowedOrigins, webhooks, access, les hooks… Un seul ajout : quand une table SitePing manque, la 500 vous indique de lancer npx prisma db push (un describeError que vous passez a la priorité). Leurs types (SitepingAccessControl, SitepingLifecycleHooks, SitepingLogger…) sont réexportés ici : un gestionnaire de paquets strict comme pnpm n'a pas besoin de @siteping/server en dépendance directe pour typer une politique à part.
Schéma de base de données
Voici le schéma exact dont l'adapter a besoin — c'est ce que génère npx @siteping/cli sync. Préférez le CLI ; si vous l'écrivez à la main, ne retirez pas de colonnes : l'adapter écrit urlPattern, screenshotUrl et anchorKey à chaque insertion, donc un schéma partiel échoue dès le premier envoi.
model SitepingFeedback {
id String @id @default(cuid())
projectName String
type String
message String @db.Text
status String @default("open")
url String
urlPattern String?
screenshotUrl String? @db.Text
screenshotRegion Json?
diagnostics Json?
viewport String
userAgent String
authorName String
authorEmail String
clientId String @unique
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
annotations SitepingAnnotation[]
comments SitepingComment[]
@@index([projectName])
@@index([projectName, status, createdAt])
@@index([projectName, url])
}
model SitepingAnnotation {
id String @id @default(cuid())
feedbackId String
feedback SitepingFeedback @relation(fields: [feedbackId], references: [id], onDelete: Cascade)
cssSelector String @db.Text
xpath String @db.Text
textSnippet String @db.Text
elementTag String
elementId String?
textPrefix String @db.Text
textSuffix String @db.Text
fingerprint String
neighborText String @db.Text
anchorKey String?
xPct Float
yPct Float
wPct Float
hPct Float
scrollX Float
scrollY Float
viewportW Int
viewportH Int
devicePixelRatio Float @default(1)
createdAt DateTime @default(now())
@@index([feedbackId])
}
model SitepingComment {
id String @id @default(cuid())
feedbackId String
feedback SitepingFeedback @relation(fields: [feedbackId], references: [id], onDelete: Cascade)
body String @db.Text
authorName String
authorEmail String
authorRole String @default("client")
clientId String @unique
createdAt DateTime @default(now())
@@index([feedbackId, createdAt])
}
@db.Textcible PostgreSQL et MySQL. SQLite et CockroachDB le refusent, et unStringsimple y est déjà sans limite —synclit leproviderde votre datasource et y omet@db.Text. Vous écrivez le schéma à la main ? Retirez ces attributs vous-même.
Sur MySQL, un
Stringsimple est unVARCHAR(191), plus court que ce qu'accepte le serveur pourprojectName,url,urlPattern,userAgent, les noms, les e-mails etclientId, ainsi que pour lefingerprintet l'anchorKeyd'une annotation. Une valeur qui n'y tient pas — le user agent du navigateur intégré d'Instagram ou de Facebook, qui dépasse 200 caractères, une URL de page complète venue degetPageScope— fait répondre auPOSTun422, que le widget ne rejoue pas, et le handler journalise[siteping] A value is too long for the store.
Fils de discussion
Le modèle SitepingComment contient les fils de discussion. Un schéma synchronisé avant lui continue de fonctionner : sans le modèle, le client généré n'a pas de délégué sitepingComment, donc PrismaStore lit chaque fil vide et le handler répond 501 aux écritures de commentaires. Pour activer les fils :
npx @siteping/cli sync # ajoute SitepingComment et SitepingFeedback.comments
npx prisma db push # ou : npx prisma migrate dev --name siteping-comments
npx prisma generate # Prisma 7 : aucune des commandes ci-dessus ne régénère le clientSur Prisma 5 et 6, db push et migrate dev régénèrent aussi le client ; Prisma 7 laisse cela à prisma generate, sans effet néfaste sur 5 et 6. Oubliez-le sur 7 et le client n'a pas de délégué sitepingComment : les fils restent désactivés, sans rien dans les logs. Appliquez le schéma à la base avant de déployer ce client : il lit la table des commentaires à chaque liste, donc tant qu'elle n'existe pas, les listes répondent 500 avec l'indication de table manquante.
Un fil revient du plus ancien au plus récent. Un envoi se rattache à son feedback, donc un feedback inconnu donne un 404 quel que soit le provider, et le clientId unique fait qu'un envoi rejoué renvoie le commentaire enregistré. La limite de 100 commentaires client est un comptage avant l'insertion : des envois qui se disputent la dernière place libre peuvent la dépasser.
Stockage des captures
Par défaut, les captures sont stockées en ligne sous forme d'URL data: dans la colonne screenshotUrl — parfait pour essayer, lourd pour une vraie base. En production, branchez un ScreenshotStorage. @siteping/screenshot-storage en fournit un pour Cloudflare R2, AWS S3 et les autres buckets compatibles S3, Cloudflare Images, votre disque ou votre base de données :
import { createScreenshotStorage } from "@siteping/screenshot-storage";
import { createS3ObjectStore } from "@siteping/screenshot-storage/s3";
createSitepingHandler({
prisma,
screenshotStorage: createScreenshotStorage(
createS3ObjectStore({ endpoint, bucket, accessKeyId, secretAccessKey, publicBaseUrl }),
),
});Ou écrivez le vôtre :
createSitepingHandler({
prisma,
screenshotStorage: {
async upload(dataUrl, { mimeType }) {
const url = await uploadToS3(dataUrl, `siteping/${crypto.randomUUID()}`, mimeType);
return { url };
},
},
});Donnez à chaque envoi une URL qu'aucun autre envoi n'obtient : une clé aléatoire neuve, jamais dérivée de feedbackId — ici c'est le clientId du client, que chaque nouvel essai renvoie — ni du contenu de l'image.
Si un envoi échoue, le feedback est quand même enregistré (avec screenshotUrl: null) et un avertissement est journalisé — un bucket cassé ne fait jamais perdre le commentaire d'un client.
Renvoyez une URL https: : la vue détail du widget n'affiche que les images https:, plus le http: simple sur la machine locale (localhost, *.localhost, 127.0.0.1, [::1]), pour qu'un MinIO ou un localstack local fonctionne en développement.
Ajoutez un delete(url) optionnel et l'adapter l'appelle à la suppression d'un feedback — DELETE unitaire comme deleteAll — pour ne rien laisser traîner dans votre bucket. C'est du meilleur effort : un delete qui échoue est journalisé et ne fait jamais échouer la suppression (un objet orphelin vaut mieux qu'une ligne qui refuse de partir). Les URL data: en ligne ne lui sont jamais passées. Au plus 32 suppressions tournent en même temps : une suppression de projet libérant des milliers d'objets ne sature pas votre stockage et n'épuise pas les sockets d'une fonction serverless. La suppression les attend : avec des milliers de captures et un bucket lent ou injoignable, elle peut dépasser la durée maximale d'une fonction serverless, et les objets qu'elle n'a pas encore supprimés restent dans le bucket.
Il est aussi appelé quand une création est refusée parce que son clientId est déjà enregistré (un nouvel essai en concurrence avec le premier), pour supprimer la capture envoyée par cette tentative — sauf si le feedback enregistré pointe vers cette même URL. C'est le cas avec une clé dérivée de feedbackId : le nouvel essai a réécrit l'objet du feedback enregistré, qui est donc conservé — et a remplacé son image, ce qu'une clé aléatoire évite. Une capture dont l'insertion a échoué pour une autre raison reste dans votre bucket, car un nouvel essai sur une autre instance peut être sur le point de la référencer.
Utiliser un store personnalisé
Pas sur Prisma ? Montez votre store directement avec @siteping/server — même handler, rien de Prisma à installer. createSitepingHandler({ store }) fonctionne toujours ici aussi : @prisma/client est une dépendance peer optionnelle, installée seulement si vous passez prisma.
screenshotStorage et caseInsensitiveSearch ne s'appliquent qu'au chemin Prisma intégré ; avec un store personnalisé, gérez les captures à l'intérieur de votre store.
Choisir un adapter
Où vivent vos feedbacks — Prisma ou Drizzle pour la production, memory pour les tests et démos, localStorage pour le tout-client.
Adapter Drizzle
Un store de production sur PostgreSQL ou Turso / libSQL via Drizzle ORM — des tables qui vivent dans votre schéma, des migrations via drizzle-kit.