SDK · End-user evaluation
Preview@sonenta/feedback
Let your own end users rate (5★) and suggest translations from inside your shipped app. React and React Native plug into the @sonenta/*-i18n provider you already run (no second context, no host re-render); anything else goes through /core, the frozen client every binding wraps. Same wire, same server-minted session, same moderation back office. Available as a Pro+ paid add-on.
The @sonenta/feedback package ships with the End-user translation evaluation add-on at the Sonenta V1 launch. The wire contract is frozen (v3); the framework bindings are still stabilising and may change before launch.
1. Install (at launch)
One package. Import the entry for your framework: @sonenta/feedback/react, /native (RN/Expo), /vue, /svelte, or /core for anything else. vue / svelte are optional peer deps, only the matching entry needs them. Published with the add-on at the V1 launch.
terminal 1// ships with the End-user evaluation add-on at the V1 launch2npm i @sonenta/react-i18next @sonenta/feedback3// feedback peers with @sonenta/react-i18next 2.x4// optional peer deps only for the matching entry: vue · svelte 2. React (web), i18n plugin
Add feedbackPlugin() to your existing @sonenta/react-i18next (>= 0.7.0) provider's plugins slot, no new provider, no second context. The provider calls the plugin's setup() once and reuses its own apiBase / projectId / defaultLocale. The panel mounts as an isolated sibling leaf with a private open/close store, so opening it never re-renders your host tree. Trigger from your own CTA via the controller delivered through controllerRef (or the onReady callback).
main.tsx 1// src/main.tsx, plugin of the i18n provider you already run2import { SonentaProvider } from "@sonenta/react-i18next";3import { feedbackPlugin } from "@sonenta/feedback/react";4import { useRef } from "react"; 6const feedback = useRef(null); 8<SonentaProvider9 projectUuid="proj_xxx"10 token={import.meta.env.VITE_SONENTA_TOKEN}11 plugins={[ feedbackPlugin({ controllerRef: feedback }) ]}12>13 <App />14</SonentaProvider> 16// own CTA, does NOT re-render the host tree17<button onClick={() => feedback.current?.open()}>Rate translations</button> All feedbackPlugin() / createFeedback() options
| Option | Type | Default |
|---|---|---|
| controllerRef | Ref<Controller> | , |
| onReady | (c) => void | , |
| keys | string[] | auto-discovered |
| flushDebounceMs | number | 1500 |
| maxBatch | number | 20 |
| defaultButton | boolean | false |
3. React Native / Expo
Identical pattern from the /native entry: add feedbackPlugin() to the same @sonenta/react-i18next provider's plugins slot in your Expo app and trigger via the controller. No extra native modules; token storage uses the platform secure store.
App.tsx 1// App.tsx (Expo / React Native), same plugins slot2import { SonentaProvider } from "@sonenta/react-i18next";3import { feedbackPlugin } from "@sonenta/feedback/native"; 5<SonentaProvider6 projectUuid="proj_xxx"7 token={process.env.EXPO_PUBLIC_SONENTA_TOKEN}8 plugins={[ feedbackPlugin({ onReady: (c) => (ctrl = c) }) ]}9>{/* … */}</SonentaProvider> 11// wire ctrl.open() to your own button / FAB 4. Anything else, /core
@sonenta/feedback/core exposes the frozen FeedbackClient all adapters are built on: acceptTos(), loadStrings(), rate(), suggest(), debounced batched transport, rotating JWT. Use it directly for any framework without a first-party adapter.
feedback.ts 1// any framework, the frozen client all adapters wrap2import { FeedbackClient } from "@sonenta/feedback/core"; 4const client = new FeedbackClient({5 apiBase: "https://api.sonenta.dev",6 projectId: "proj_xxx", language: "fr",7 // REQUIRED. Without it, the first authenticated call8 // accepts the end-user ToS on your user's behalf.9 autoAcceptTos: false,10}); 12// show YOUR ToS step, and only once the user agrees:13await client.acceptTos(); // server mints the session14await client.loadStrings(); client.rate(/* … */); client.suggest(/* … */); 16// with autoAcceptTos:false, an unconsented authed call throws17// FeedbackError("not consented") instead of fabricating a record. 5. Rendered-key scoping (automatic)
The panel auto-scopes to the keys actually rendered on the current view, via the global key registry the @sonenta/*-i18n SDK produces, no configuration. Pass an explicit keys array only as a fallback (e.g. strings not sourced from @sonenta/*-i18n); never pass your whole catalogue, that would surface every string in the app, not the ones the user is looking at. The registry is mount-tracked and ref-counted: persistent always-on-screen strings (a header, an eyebrow) stay registered while their component is mounted, there is no per-view reset. (reset() exists only as an escape hatch for non-React routing edge cases; the SDK never calls it automatically.)
scoping.ts 1// the panel auto-scopes to keys RENDERED on the current2// view, via the global key registry the @sonenta/*-i18n3// SDK produces, no config needed:4feedbackPlugin({ controllerRef: feedback }); // auto-scoped 6// explicit keys = FALLBACK only (e.g. strings not from7// @sonenta/*-i18n). NEVER pass your whole catalogue.8feedbackPlugin({ keys: ["common:checkout.cta"] }); 6. Namespace filter (optional)
A screen that renders several namespaces can scope the panel to just the one the customer cares about, pass an optional namespace (string | string[]) on the trigger/config (feedbackPlugin() for React/Native, createFeedback() for Vue/Svelte, resolveKeys() / filterByNamespace() for /core). It composes after rendered-scoping, shown = rendered ∩ namespace. Unset, "", or [] means no filter (identical to before). It never falls back to the whole project.
namespace.ts 1// §0d, OPTIONAL namespace filter (customer feature).2// Composes AFTER rendered-scoping: shown = rendered ∩ namespace.3feedbackPlugin({ controllerRef: feedback, namespace: "quiz" }); 5// Vue / Svelte, same option on createFeedback:6createFeedback({ apiBase, projectId, language, namespace: ["quiz"] }); 8// /core, resolveKeys / filterByNamespace:9resolveKeys(explicit, "quiz"); // or filterByNamespace(keys, "quiz") 11// unset / "" / [] ⇒ no filter (identical to v5). 7. Server-minted session (all frameworks)
The session / grouping key is minted server-side at consent. The client never sends or self-generates it, there is no groupingKey config and no request field. On acceptTos() the backend returns it (bound into the scoped JWT); every adapter exposes it read-only as client.sessionId. A returning endUserId keeps its stable server value; a new end user gets a fresh sess_….
consent.ts 1// session/grouping key is MINTED SERVER-SIDE at consent2await client.acceptTos(); // POST /v1/feedback/tos3client.sessionId; // read-only, e.g. "sess_018f…" 5// NO groupingKey config, NO request field, 6// the client never sends or self-generates the session. 8. Consent & security
A versioned Sonenta end-user ToS gates the first write. In the React and React Native panels the ToS step renders before any authenticated call, and the end user’s own tap is what calls acceptTos(): the string fetch is gated on consent by construction. Going through /core instead, construct the client with autoAcceptTos: false and call acceptTos() only once your user has agreed, otherwise the first authenticated call accepts on their behalf. The client then holds a short-lived JWT scoped to feedback:write only, cryptographically separate from your customer auth, and rotates it transparently. End users are anonymous (opaque id, no PII). On @sonenta/feedback 1.2.x and earlier this is the DEFAULT behaviour: the client accepts on the first authenticated call unless you pass autoAcceptTos: false. Version 1.3.0 flips that default and fails closed, throwing FeedbackError("not consented") rather than accepting for your user.
What you get for free
- Zero host re-render. React/Native run as an isolated sibling leaf of the i18n provider; Vue/Svelte keep open-state in their own store. Adding or opening feedback never re-renders your app.
- Consent + server session handled. ToS acceptance, server-minted session, JWT acquisition, rotating refresh, and a one-shot transparent retry on 401, all inside the SDK. It never throws into your render path.
- Debounced, batched transport. Ratings and suggestions are queued and flushed on a debounce (default 1.5s), at a max batch, or on close. Best-effort: a failed batch is re-queued once, then swallowed.
- Moderation before publish. Nothing an end user submits goes live automatically. Suggestions land as
pendingin your dashboard's moderation queue, you approve, reject, or apply through the normal audited edit path. Ratings roll up into a realtime per-key / per-language dashboard.