Adaptateurs

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-prisma

Le 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

OptionTypeDéfautCe que ça fait
prismaSitepingPrismaClient—Votre client Prisma. Obligatoire sauf si vous passez store
storeSitepingStore—Utiliser n'importe quel store au lieu de Prisma (prioritaire)
screenshotStorageScreenshotStorage—Envoyer les captures quelque part de réel au lieu de les inliner en URL data:. Ignoré quand vous passez un store personnalisé
caseInsensitiveSearchbooleandétecté automatiquementDé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.Text cible PostgreSQL et MySQL. SQLite et CockroachDB le refusent, et un String simple y est déjà sans limite — sync lit le provider de votre datasource et y omet @db.Text. Vous écrivez le schéma à la main ? Retirez ces attributs vous-même.

Sur MySQL, un String simple est un VARCHAR(191), plus court que ce qu'accepte le serveur pour projectName, url, urlPattern, userAgent, les noms, les e-mails et clientId, ainsi que pour le fingerprint et l'anchorKey d'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 de getPageScope — fait répondre au POST un 422, 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 client

Sur 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.

Modifier sur GitHub

Sur cette page