@conclude-fyi/react
v0.8.1
Published
React SDK for Conclude.fyi — feedback collection with screenshots, screen recording, and annotation
Maintainers
Readme
@conclude-fyi/react
React SDK for Conclude.fyi — AI-native feedback collection with screenshots, screen recording, and annotation.
Install
npm install @conclude-fyi/reactQuick Start
"use client";
import { ConcludeProvider, FeedbackButton } from "@conclude-fyi/react";
function App() {
return (
<ConcludeProvider
apiKey={process.env.NEXT_PUBLIC_CONCLUDE_API_KEY!}
user={{ id: "user-123", name: "Jane", email: "[email protected]" }}
>
<FeedbackButton />
</ConcludeProvider>
);
}That's it. The API key encodes which board to use — no boardId, no appUrl.
Get your API key: [Your Product] → Settings → Board tab → Widget snippet in your Conclude dashboard.
Features
- Native rendering — renders in your React tree via Shadow DOM for CSS isolation, no iframe
- Screenshots — instant page capture via
html-to-imagewith an annotation editor (arrows, rectangles, text, freehand, color picker) - Screen recording — composites screen + face cam + mic into a single video via
getDisplayMedia+video-stream-merger - AI auto-classification — every submission is server-side tagged with type (feature/bug/kudos/question), severity, descriptive category, and sentiment. No user-facing picker required.
- Theming — auto-fetches theme, accent color, label, modes, and email field from your dashboard
- Post-submission engagement — shows trending feedback items for one-tap voting
- Privacy — password inputs always masked, opt-in masking via
data-conclude-mask - Error handling — clear console errors if your key is wrong, CSP blocks the request, or the board can't be found
Components
<ConcludeProvider>
Wraps your app and provides config to all child components.
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| apiKey | string | Yes | Board-scoped publishable key (pk_live_...) from your board's Embed tab |
| apiHost | string | No | Override API host. Defaults to https://www.conclude.fyi; use a canonical non-redirecting origin |
| user | ConcludeUser | No | Identified user |
| theme | "dark" \| "light" \| "auto" | No | Theme override (defaults to dashboard config) |
The default API host intentionally uses https://www.conclude.fyi. Browser CORS preflight requests do not follow redirects, so avoid pointing apiHost at an origin that redirects.
User object
{
id: string; // Recommended — your app's stable user ID (Clerk sub, Auth0 sub, internal UUID). Opaque, no PII.
name?: string;
email?: string; // Optional. Acts as the submitter ID if `id` is omitted, but stores PII server-side. Prefer `id` when you have one.
company?: string;
plan?: string;
properties?: Record<string, unknown>;
}Why prefer id over email: when both are passed, the SDK uses id as the submitter identifier and email is just associated metadata. When only email is passed, the email itself becomes the submitter ID and is stored in the database as the primary key for that user across submissions. Most apps already have a stable internal user ID — passing it as id lets Conclude store an opaque identifier instead of an email, which simplifies data subject requests and reduces PII exposure.
<FeedbackButton />
Floating feedback button. Opens a native panel with mode selector, form, screenshot capture, and screen recording.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| position | "bottom-right" \| "bottom-left" | Dashboard config | Button position |
| label | string | Dashboard config | Button label |
| accentColor | string | Dashboard config | Accent color (hex) |
All props are optional — values are fetched from your dashboard widget settings. Automatically hides itself if the API key is invalid or the SDK can't reach Conclude (with a console error explaining why).
<FeedbackBoard />
Inline feedback board with voting (renders via iframe).
| Prop | Type | Default |
|------|------|---------|
| sort | "most_voted" \| "newest" \| "trending" | "most_voted" |
| className | string | — |
<FeedbackForm />
Standalone form for custom integrations.
| Prop | Type | Default |
|------|------|---------|
| onSubmit | (item: FeedbackItem) => void | — |
| placeholder | string | "What would you like to share?" |
| submitLabel | string | "Submit feedback" |
useConclude()
Hook for programmatic submission.
const { submitFeedback, isReady } = useConclude();
await submitFeedback({ title: "Feature request", body: "..." });<DocsLink />
Small link-as-button for sending users to your help docs. Drop it in nav bars, help menus, empty states — anywhere. Clicking it opens the target URL in a new tab.
import { DocsLink } from "@conclude-fyi/react";
<DocsLink href="https://docs.myapp.com" />
<DocsLink href="..." label="Help" variant="ghost" theme="dark" />
<DocsLink href="..." variant="default" accentColor="#6366f1" iconOnly />| Prop | Type | Default | Description |
|------|------|---------|-------------|
| href | string | — | Target URL (required) |
| label | string | "Docs" | Button text |
| variant | "default" \| "outline" \| "ghost" | "outline" | Visual style — mirrors shadcn's Button variants |
| size | "default" \| "sm" \| "lg" | "default" | Padding + font size |
| accentColor | string | "#6366f1" | Accent color for default and outline variants |
| theme | "light" \| "dark" \| "auto" | "auto" | Color palette. auto follows OS preference |
| iconOnly | boolean | false | Render just the icon |
| icon | ReactNode | book icon | Custom leading icon |
| className | string | — | Additional class for consumer overrides |
| style | CSSProperties | — | Inline style merged over the variant |
| onClick | (e) => void | — | Intercept clicks (e.g. for analytics) before the link opens |
Moments — event-driven micro-feedback
Moments are in-app thumbs up/down prompts fired at meaningful user actions — the instant someone publishes, completes onboarding, or finishes a flow. Captured at the moment of truth, not days later from memory.
Quick start
import { ConcludeProvider, useMoment } from "@conclude-fyi/react";
function PublishButton({ onPublish }: { onPublish: () => void }) {
const { capture } = useMoment();
return (
<button onClick={async () => {
await onPublish();
capture("post_published"); // fire-and-forget
}}>
Publish
</button>
);
}
function App() {
return (
<ConcludeProvider apiKey={process.env.NEXT_PUBLIC_CONCLUDE_API_KEY!}>
<PublishButton onPublish={() => fetch("/api/publish", { method: "POST" })} />
</ConcludeProvider>
);
}How it works
- On mount the hook fetches the set of active Moments your team has configured for this board.
capture(eventName)checks client-side cooldown + sampling, then shows a thumbs up/down toast in a Shadow DOM overlay.- On interaction the SDK records the response to Conclude. The moment feedback flows through the same AI + theming pipeline as widget feedback (with
source: "moment").
Event names are configured in the Conclude dashboard before the SDK can prompt for them. An unknown event is recorded as an observed event (visible to your PM for curation) but shows no prompt.
useMoment()
| Returns | Type | Description |
|---------|------|-------------|
| capture | (eventName: string, options?: { metadata?: Record<string, unknown> }) => Promise<void> | Fire a moment. Silent no-op if the event isn't configured, is on cooldown, or is sampled out. |
Sampling and cooldown
Both are configured per Moment in the dashboard.
- Sampling rate (1–100%): percentage of eligible fires that actually show a prompt. 100% by default.
- Per-user cooldown (hours, 0–168): minimum time between prompts for the same event to the same user. 24h by default.
Cooldown is tracked client-side in localStorage keyed by event name. If localStorage is unavailable (private browsing, quota exceeded), cooldown silently degrades to "no cooldown" — prompts may show more often than expected for that user, but nothing breaks.
Identity
Recommended: pass an opaque user.id (your Clerk user ID, Auth0 sub, internal user UUID — anything stable and non-PII). The SDK uses it as the submitter identifier and stores it server-side in place of email. This simplifies data-subject requests and reduces PII exposure.
- If you pass
user.id, that's the submitter ID. - If only
user.emailis provided, the email becomes the submitter ID (stored as PII). - If neither is provided, the SDK generates a random anonymous ID on first capture and persists it in
localStorage. - The cooldown key is the event name, not keyed by submitter. Logging out and back in as someone else in the same browser does not reset cooldown for events already fired.
- Moments configured with once per user server-side dedup require an identified submitter (
user.idoruser.email) — anonymous fires fall back to per-Moment cooldown only.
Ordering: cooldown records before the network call
The SDK writes the cooldown entry locally before POSTing the capture. This means a failed network call still prevents re-prompting for the cooldown window — deliberate, to avoid hammering users when the backend has a transient outage. Trade-off: captures sent during a Conclude outage are not retried.
Privacy
eventNameand anymetadataobject you pass are sent to Conclude.- Thumbs-down follow-up text is submitter-supplied. Conclude's AI pipeline may use it to generate themes + sentiment embeddings. Same data-flow as widget feedback.
- No page content, URLs, or referrer data is collected by
useMomentitself.
Rate limits
- Discovery (one call per unknown event per SDK mount): 60/min per workspace.
- Capture submission: 60/min per workspace.
- Bootstrap (one call per mount): no rate limit.
The SDK de-duplicates discovery per session, so these limits only kick in under pathological re-mounting.
Console output (development)
When NODE_ENV !== "production" and the Moments bootstrap fetch fails (ad-blocker, invalid key, network down), the SDK logs:
[Conclude] Moments bootstrap failed — prompts disabled. Check API key/network.No output in production builds.
Theming
The toast honors the theme prop on <ConcludeProvider> ("light" | "dark" | "auto", default "auto"). "auto" resolves to the user's OS preference at mount.
Editing prompts without a redeploy
Prompt copy, sampling, cooldown, enabled state, and follow-up behavior are all configured in the dashboard. SDK only cares about the eventName string. Changes apply on the next SDK mount in the user's session.
Next.js App Router
// app/providers.tsx
"use client";
import { ConcludeProvider, FeedbackButton } from "@conclude-fyi/react";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ConcludeProvider apiKey={process.env.NEXT_PUBLIC_CONCLUDE_API_KEY!}>
{children}
<FeedbackButton />
</ConcludeProvider>
);
}# .env.local
NEXT_PUBLIC_CONCLUDE_API_KEY=pk_live_...Content Security Policy
If your app sends a CSP header, add these directives:
connect-src 'self' https://www.conclude.fyi;
img-src 'self' https://www.conclude.fyi blob: data:;
media-src 'self' https://www.conclude.fyi blob:;connect-src— SDK fetches widget config and submits feedbackimg-src— screenshot previews and dashboard-configured logosmedia-src— recording mode playback
No frame-src or script-src needed — the React SDK renders natively (no iframe, no external scripts).
See the full CSP guide for Next.js and meta-tag examples.
Error handling
The SDK logs clear errors to the browser console if anything goes wrong:
| Problem | Console message | UI behavior |
|---------|----------------|-------------|
| Missing or malformed apiKey | [Conclude] Missing apiKey prop on <ConcludeProvider>. | Button hidden |
| Invalid API key (401/403) | [Conclude] Invalid API key. Check that you're using the board-scoped pk_live_ key... | Button hidden |
| Board deleted or internal (404) | [Conclude] Board not found... | Button hidden |
| CSP, network, or preflight redirect blocks the request | [Conclude] Could not reach Conclude. This can be caused by a network failure, Content Security Policy, or a CORS preflight redirect... | Button hidden |
| Server error (5xx) | [Conclude] Server returned {status}... | Button hidden |
<FeedbackBoard /> shows an inline error message instead of hiding (since it's inline content).
Architecture
The SDK renders natively in your React tree using Shadow DOM for CSS isolation. No iframe, no external scripts.
- Screenshots:
html-to-imagecaptures the DOM - Annotations:
@markerjs/markerjs3provides arrow, rectangle, text, and freehand tools - Recording:
getDisplayMedia+video-stream-mergercomposites screen + camera + audio into a WebM video - Theming: fetched from your dashboard config API on mount, keyed by your board API key
License
MIT
