SDK Reference/Next.js
Framework
@swake/web

Next.js

Integration guide for Next.js. Uses the @swake/web SDK, which works with any JavaScript framework or vanilla JS.

Overview

@swake/web is a framework-agnostic feedback SDK — one core client plus an optional UI widget layer (@swake/web/ui) that renders a trigger button, feedback form, history panel, and survey modals inside a Shadow DOM. In Next.js the SDK is browser-only — it must be initialised from a client component, whether via the App Router or Pages Router (see Requirements below for details).

A few concepts come up throughout this guide:

ConceptWhat it is
apiKeyYour project's key, the only required option to Swake.init().
identify()Links feedback, surveys, and notifications to a user. Almost everything downstream depends on it.
Offline queueSubmissions are queued in localStorage and retried automatically — this happens for every call, with no extra code.
ThemeA single theme object, referenced by nearly every widget, applied as CSS custom properties.

Getting Started

Requirements

Next.js13.0 or later (App Router or Pages Router)
React18.0 or later
BrowserChrome 80+, Firefox 78+, Safari 14+, Edge 80+
Node.js18+ (for build tooling only — SDK runs in browser)

Swake is a browser-only SDK. In Next.js SSR/SSG environments, it must run exclusively on the client. Use 'use client' directives and dynamic imports.


Installation

bash
npm install @swake/web-swake-sdk
# or
yarn add @swake/web-swake-sdk
# or
pnpm add @swake/web-swake-sdk

The package exports two entry points:

ImportPurpose
@swake/webCore SDK — init, identify, submit, notifications, surveys, breadcrumbs
@swake/web/uiVisual widgets — trigger FAB, feedback form, feedback history panel, survey modals (rendered in Shadow DOM)

The @swake/web/ui import is a side-effect — just import it once and it automatically hooks into the SDK. No extra setup required.


App Router Setup

Create a client component that initialises Swake, then add it to your root layout. This is the minimal setup to get a working trigger button and feedback form.

app/providers/SwakeProvider.tsx
'use client';

import { useEffect } from 'react';
import Swake from '@swake/web';

export function SwakeProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    import('@swake/web/ui').then(() => {
      Swake.init({
        apiKey: process.env.NEXT_PUBLIC_SWAKE_API_KEY!,
        appVersion: process.env.NEXT_PUBLIC_APP_VERSION,
      });
    });
  }, []);

  return <>{children}</>;
}
app/layout.tsx
import { SwakeProvider } from './providers/SwakeProvider';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <SwakeProvider>{children}</SwakeProvider>
      </body>
    </html>
  );
}

Full init options

Everything except apiKey is optional.

typescript
Swake.init({
  apiKey: 'ep_live_your_api_key',        // required
  baseUrl: 'https://api.swake.io',       // optional — override for self-hosted or a proxy
  debug: false,                          // enable verbose console logs
  appVersion: '1.0.0',                   // shown in the portal per submission
  autoCapture: {
    browserInfo: true,                   // attach browser/OS info to submissions
  },
  surveys: {
    enabled: true,                       // fetch & show eligible surveys
    autoShow: true,                      // auto-show after identify()
    delay: 3000,                         // ms before auto-show
    cooldown: 86_400_000,                // ms between surveys (default: 24h)
  },
  breadcrumbs: {
    enabled: true,                       // attach breadcrumb trail to submissions
    maxEntries: 50,                      // ring buffer size (max 100)
    captureNavigation: true,             // History API pushState/popstate
    captureNetwork: true,                // fetch() and XMLHttpRequest
    captureConsole: true,                // console.warn / console.error
    captureClicks: true,                 // element click events
  },
  trigger: {
    position: 'bottom-right',            // 'bottom-left' | 'top-right' | 'top-left'
    icon: 'chat',                        // 'feedback' | 'bug' | 'idea'
    label: 'Feedback',                   // tooltip text
    hidden: false,                       // start hidden, show later
    zIndex: 999999,
    style: {
      backgroundColor: '#059669',
      size: 56,                          // px
      borderRadius: 28,                  // px
      shadow: true,
    },
  },
  theme: { /* see Theme Customization section */ },
  onLimitReachedMode: 'default_ui',      // 'silent' | 'callback_only'
  onLimitReached: (event) => {
    console.warn(event.limitKey, event.message);
  },
});
Full reference:

Pages Router Setup

pages/_app.tsx
import type { AppProps } from 'next/app';
import { useEffect } from 'react';
import Swake from '@swake/web';

export default function App({ Component, pageProps }: AppProps) {
  useEffect(() => {
    import('@swake/web/ui').then(() => {
      Swake.init({ apiKey: process.env.NEXT_PUBLIC_SWAKE_API_KEY! });
    });
  }, []);

  return <Component {...pageProps} />;
}

Context Pattern

For production apps we recommend wrapping the SDK lifecycle in a React context. This gives you a clean useSwake() hook, handles init/destroy on mount/unmount, polls for unread notifications, and listens for survey events.

app/providers/SwakeContext.tsx
'use client';

import React, {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
} from 'react';
import Swake from '@swake/web';
import type { SwakeWebTheme } from '@swake/web';

interface SwakeCtx {
  initialized: boolean;
  userId: string | null;
  unreadCount: number;
  identify: (userId: string, email?: string, name?: string) => Promise<void>;
  logout: () => void;
  refreshUnread: () => Promise<void>;
}

const Ctx = createContext<SwakeCtx | null>(null);

export function useSwake(): SwakeCtx {
  const ctx = useContext(Ctx);
  if (!ctx) throw new Error('useSwake must be inside <SwakeProvider>');
  return ctx;
}

interface Props {
  apiKey: string;
  theme?: SwakeWebTheme;
  children: React.ReactNode;
}

export function SwakeProvider({ apiKey, theme, children }: Props) {
  const [initialized, setInitialized] = useState(false);
  const [userId, setUserId] = useState<string | null>(null);
  const [unreadCount, setUnreadCount] = useState(0);
  const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);

  // Init + cleanup
  useEffect(() => {
    import('@swake/web/ui').then(() => {
      Swake.init({
        apiKey,
        appVersion: '1.0.0',
        surveys: { enabled: true, autoShow: true, delay: 2000 },
        breadcrumbs: {
          captureNavigation: true,
          captureNetwork: true,
          captureConsole: true,
          captureClicks: true,
        },
        trigger: { position: 'bottom-right' },
        theme,
      });
      setInitialized(true);
    });

    return () => {
      Swake.destroy();
      setInitialized(false);
    };
  }, [apiKey, theme]);

  // Unread notification polling (every 60s while identified)
  const refreshUnread = useCallback(async () => {
    if (!userId) return;
    try {
      const count = await Swake.getUnreadCount();
      setUnreadCount(count);
    } catch { /* non-fatal */ }
  }, [userId]);

  useEffect(() => {
    if (!userId) { setUnreadCount(0); return; }
    void refreshUnread();
    pollRef.current = setInterval(refreshUnread, 60_000);
    return () => { if (pollRef.current) clearInterval(pollRef.current); };
  }, [userId, refreshUnread]);

  // Identity helpers
  const identify = useCallback(async (uid: string, email?: string, name?: string) => {
    await Swake.identify({ userId: uid, email, name });
    setUserId(uid);
  }, []);

  const logout = useCallback(() => {
    Swake.clearIdentity();
    setUserId(null);
    setUnreadCount(0);
  }, []);

  return (
    <Ctx.Provider value={{ initialized, userId, unreadCount, identify, logout, refreshUnread }}>
      {children}
    </Ctx.Provider>
  );
}

Use in your layout

app/layout.tsx
import { SwakeProvider } from './providers/SwakeContext';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <SwakeProvider apiKey={process.env.NEXT_PUBLIC_SWAKE_API_KEY!}>
          {children}
        </SwakeProvider>
      </body>
    </html>
  );
}

Use the hook in components

app/components/Header.tsx
'use client';

import { useSwake } from '../providers/SwakeContext';

export function Header() {
  const { userId, unreadCount, identify, logout } = useSwake();

  return (
    <header>
      {userId ? (
        <>
          <span>Notifications: {unreadCount}</span>
          <button onClick={logout}>Log out</button>
        </>
      ) : (
        <button onClick={() => identify('user_123', 'user@example.com', 'Jane')}>
          Log in
        </button>
      )}
    </header>
  );
}

Identifying Users

Call Swake.identify() after your user logs in. This links all feedback, survey responses, and notifications to that user in the Swake portal.

app/components/AuthHandler.tsx
'use client';

import { useEffect } from 'react';
import { useSession } from 'next-auth/react'; // or your auth lib
import Swake from '@swake/web';

export function AuthHandler() {
  const { data: session } = useSession();

  useEffect(() => {
    if (session?.user) {
      Swake.identify({
        userId: session.user.id,              // required — your internal user ID
        email: session.user.email ?? undefined, // optional but recommended
        name: session.user.name ?? undefined,   // optional
        metadata: {                             // optional — custom traits
          plan: 'pro',
          company: 'Acme Corp',
        },
      });
    } else {
      Swake.clearIdentity();
    }
  }, [session]);

  return null;
}
ParameterTypeDescription
userIdstringYour app's unique user ID (required)
emailstringUser's email. Preserved if omitted on subsequent calls.
namestringUser's display name. Preserved if omitted on subsequent calls.
metadataRecord<string, string>Custom key-value pairs. Fully replaced on each call — send the complete set.

email and name are preserved server-side if omitted on subsequent calls. metadata is fully replaced on every call.

Clearing identity on logout

typescript
Swake.clearIdentity();

This wipes the stored user ID and clears the offline queue. Always call this when the user logs out.


Feedback

Feedback Form

Open the built-in feedback form widget. Requires the @swake/web/ui import. The form includes a type selector (Bug / Idea / Help), title and description fields, and a drag-and-drop file upload zone.

typescript
import Swake from '@swake/web';

// Default form
Swake.showFeedbackForm();

// With options
Swake.showFeedbackForm({
  defaultType: 'feature',           // pre-select type: 'bug' | 'feature' | 'question'
  showTypeSelector: true,           // show/hide the Bug/Idea/Help toggle (default: true)
  title: "What's on your mind?",    // header text
  placeholder: 'Tell us more...',   // textarea placeholder
  theme: { primaryColor: '#8B5CF6' }, // per-call theme override
});
OptionTypeDefaultDescription
defaultType'bug' | 'feature' | 'question''bug'Pre-selected feedback type
showTypeSelectorbooleantrueShow the type toggle buttons
titlestring"What's on your mind?"Header text above the form
placeholderstring'Tell us more...'Description textarea placeholder
themeSwakeWebThemeglobal themeOverride theme for this widget only

Example: custom trigger button

Hide the built-in FAB and wire the form to your own button:

app/components/ReportBugButton.tsx
'use client';

import Swake from '@swake/web';

export function ReportBugButton() {
  return (
    <button onClick={() => Swake.showFeedbackForm({ defaultType: 'bug' })}>
      Report a Bug
    </button>
  );
}

// In your init config, hide the trigger:
// trigger: { hidden: true }

Submitting Feedback

Submit feedback programmatically without showing any UI. Browser info and breadcrumbs are automatically attached.

app/components/FeedbackButton.tsx
'use client';

import Swake from '@swake/web';

export function FeedbackButton() {
  const handleSubmit = async () => {
    const submission = await Swake.submitFeedback({
      type: 'bug',                                      // 'bug' | 'feature' | 'question'
      title: 'Search returns no results',                // required
      description: 'Reproducible on Safari + macOS 14.', // optional
      attachments: [                                      // optional — File objects
        { file: screenshotFile },
      ],
    });

    console.log('Created:', submission.id);
  };

  return <button onClick={handleSubmit}>Report Bug</button>;
}
ParameterTypeDescription
type'bug' | 'feature' | 'question'Submission category (required)
titlestringBrief summary (required)
descriptionstringDetailed description
attachments{ file: File }[]File objects from <input type="file">, drag-and-drop, or clipboard
customFieldsRecord<string, string>Project custom field values as a { fieldId: value } map — fetch definitions with getCustomFields(); required fields enforced

Returns a full Submission object with id, status, createdAt, and more.


Feedback History

Show a panel listing the current user's past submissions with status badges and a detail view with replies (including auto-translated team responses). Requires the user to be identified.

typescript
// Open the feedback history panel
Swake.showFeedbackHistory();

Programmatic access

You can also fetch history data directly to build your own UI:

typescript
// Lightweight list (id, title, status, type, createdAt)
const { submissions, hasMore, cursor } = await Swake.getHistory({ limit: 20 });

// Full submission detail with comments + attachments
const submission = await Swake.getSubmission('submission_id');

// Paginated submission list with more fields
const result = await Swake.listSubmissions({ limit: 10 });

Trigger Button (FAB)

When you import @swake/web/ui, a floating action button appears in the corner of your page. Clicking it opens the feedback form. Configure it via the trigger option in Swake.init().

typescript
Swake.init({
  apiKey: 'ep_live_...',
  trigger: {
    position: 'bottom-right',     // 'bottom-left' | 'top-right' | 'top-left'
    icon: 'chat',                 // 'feedback' | 'bug' | 'idea'
    label: 'Feedback',            // tooltip text
    hidden: false,                // start hidden
    zIndex: 999999,               // container z-index
    style: {
      backgroundColor: '#059669', // FAB background color
      size: 56,                   // diameter in px
      borderRadius: 28,           // px
      shadow: true,               // drop shadow
    },
  },
});

Show / hide at runtime

typescript
// Hide the FAB (e.g. during onboarding)
Swake.setTriggerVisible(false);

// Show it again
Swake.setTriggerVisible(true);

Programmatic open / close

typescript
// Open the feedback form
Swake.open();

// Close any open widget (form, history, survey)
Swake.close();

All widgets are rendered inside a closed Shadow DOM container. Your app's CSS never leaks in, and widget styles never leak out.


Engagement

Surveys & NPS

Swake can show surveys, NPS polls, and multiple-choice polls to your users. Surveys are created in the Swake portal, and the SDK handles targeting, display, and response submission.

Configuration

typescript
Swake.init({
  apiKey: 'ep_live_...',
  surveys: {
    enabled: true,        // fetch eligible surveys (default: true)
    autoShow: true,       // auto-show after identify() (default: true)
    delay: 3000,          // ms before auto-show (default: 3000)
    cooldown: 86_400_000, // ms between surveys (default: 24h)
  },
});

Manual control

typescript
// Re-check eligibility and show the first match
await Swake.checkSurveys();

// Show a specific survey by ID (ignores cooldown)
await Swake.showSurvey('survey_abc123');

Event listeners

typescript
Swake.onSurveyShown((event) => {
  console.log('Survey shown:', event.id, event.type);
});

Swake.onSurveyCompleted((event) => {
  console.log('Completed:', event.survey.id, 'answers:', event.response.answers);
});

Swake.onSurveyDismissed((event) => {
  console.log('Dismissed:', event.id);
});

Survey targeting (user traits, segments, days active) is evaluated server-side. The SDK caches eligible surveys per project for 5 minutes.


Voting Board

Let users browse and vote on public board items (feature requests, roadmap items). Fetching items does not require identification, but voting does.

typescript
import Swake from '@swake/web';

// Fetch board items
const { items, hasMore } = await Swake.getBoardItems('public-roadmap', {
  sort: 'votes',      // 'votes' | 'recent'
  status: 'open',     // 'open' | 'in_progress' | 'resolved' | 'closed'
  page: 1,            // 1-based pagination
});

// Cast a vote (requires identify)
const { status, voteCount } = await Swake.vote('public-roadmap', 'item_id');
// status: 'voted' | 'already_voted' | 'error'

// Remove a vote
const result = await Swake.removeVote('public-roadmap', 'item_id');
// result.status: 'removed' | 'not_voted' | 'error'

Notifications

Notifications

The SDK provides in-app notifications for status changes and team replies. When using @swake/web/ui, the trigger FAB shows an unread badge automatically (polled every 60 seconds).

Programmatic API

typescript
// Get unread count
const count = await Swake.getUnreadCount();

// List notifications
const { notifications, pagination } = await Swake.listNotifications({
  unreadOnly: true,  // only unread (default: false)
  limit: 20,
  cursor: undefined, // for pagination
});

// Mark as read
await Swake.markNotificationRead('notification_id');

// Mark all as read
await Swake.markAllNotificationsRead();

Polling in Next.js

The Context Pattern above handles polling automatically. If you're not using the context, poll manually with a client-side hook:

app/hooks/useUnreadCount.ts
'use client';

import { useEffect, useState } from 'react';
import Swake from '@swake/web';

export function useUnreadCount(userId: string | null) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (!userId) return;

    const poll = async () => {
      try { setCount(await Swake.getUnreadCount()); }
      catch { /* non-fatal */ }
    };

    void poll();
    const timer = setInterval(poll, 60_000);
    return () => clearInterval(timer);
  }, [userId]);

  return count;
}

Customization

Theme Customization

Customise the look of all Swake widgets to match your brand. Pass a theme object in init().

app/providers/SwakeProvider.tsx
'use client';

import { useEffect } from 'react';
import Swake from '@swake/web';

export function SwakeProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    import('@swake/web/ui').then(() => {
      Swake.init({
        apiKey: process.env.NEXT_PUBLIC_SWAKE_API_KEY!,
        theme: {
          primaryColor: '#8B5CF6',
          backgroundColor: '#FFFFFF',
          textColor: '#0F172A',
          borderRadius: 12,
        },
      });
    });
  }, []);

  return <>{children}</>;
}
TokenDefaultDescription
primaryColor#6366F1Buttons, active states, FAB background
successColor#10B981Success checkmark, resolved badges
dangerColor#EF4444Error borders, destructive actions
backgroundColor#FFFFFFModal / panel background
surfaceColor#F9FAFBInput fields, secondary surfaces
overlayColorrgba(0,0,0,0.45)Backdrop behind modals
textColor#111827Primary text
textSecondary#6B7280Labels, placeholders, metadata
borderColor#E5E7EBInput borders, dividers
borderRadius8Corner radius for inputs and buttons
fontFamilySystem defaultFont family applied to all text


Offline

Offline Queue

When the user is offline, submissions are queued in localStorage and automatically retried when connectivity returns. The queue starts processing on Swake.init() and stops on Swake.destroy().

typescript
import { offlineQueue } from '@swake/web';

// Check queue size (e.g. show a badge)
const pending = offlineQueue.size();

// Force a flush attempt
await offlineQueue.flush();

Swake.clearIdentity() clears the offline queue. Always call it on logout to prevent queued items from being sent after a different user logs in.


AI Setup

Using Claude Code

Prefer to let an AI coding agent do the wiring? Run Claude Code from the root of your Next.js project and paste the prompt below. It reads this page as the source of truth, detects your stack, asks you for your API key and anything else it needs, then wires up @swake/web, init(), identify(), the widgets, and theming — in one shot.

Prerequisites

Before you run the prompt, set up your project in the Swake portal and grab the values the agent will ask for — it only writes placeholders to your env files, so you supply the real values yourself.

  1. Sign in to the Swake portal and create a project (or open an existing one).
  2. In Project settings → API keys, get existing or create a key and copy it — it's shown once and starts with ep_live_.
  3. If you want the voting board, create a board and copy its slug from the board's URL / settings.

Have these ready when the agent prompts you — the same values you'd otherwise wire up by hand:

ValueWhere to find itExample
apiKeyProject settings → API keysep_live_…
baseUrlAPI endpoint — optional, only needed when self-hosting or routing through your own backendhttps://api.swake.io
boardSlugVoting board settings — optional; passed to getBoardItems() / vote(), not init()public-roadmap

The prompt is additive and safe: a Swake or network error must never break your app's boot or existing behavior. The agent pauses and asks before installing packages or writing secrets, and it never guesses APIs that aren't in these docs. It also asks whether to keep your ep_live_ key server-side behind your own backend — the recommended setup for any app that has one, since the key never reaches the browser and the submitting user can't be spoofed.

The prompt

Run Claude Code from your project root and paste this. It targets Next.js specifically, and re-reads this guide before it writes anything, so it stays correct as the SDK evolves.

Claude prompt
Fully integrate the Swake feedback SDK (feedback forms, bug reports, surveys/NPS, in-app notifications, voting boards) into this Next.js app. Treat it as additive: a Swake or network failure must NEVER break boot or any existing behavior. Never invent or fake an API that is not in the docs.

1. READ THE DOCS FIRST — they are the source of truth for the package, init/identify, the widget layer and every option. Fetch with a browser User-Agent (a plain fetch returns 403): curl -sL -A "Mozilla/5.0" "https://swake.io/docs/sdk?framework=nextjs". Then detect whether this app uses the App Router, the Pages Router, or both, and say which you found before you edit anything.
2. ASK BEFORE DECIDING ANYTHING (AskUserQuestion): (a) integration mode — DIRECT, meaning the ep_live_ key ships in the browser via NEXT_PUBLIC_SWAKE_API_KEY (fine for a static or SPA-only app), or PROXY, meaning the key stays server-side in a Next.js Route Handler (recommended whenever a backend exists — see step 4); (b) the API key or proxy path, an optional baseUrl, and a voting-board slug if they want the board. Write PLACEHOLDERS ONLY into env files (and the .example alongside) — never commit a real key. ASK again before installing any package.
3. INSTALL & KEEP IT CLIENT-ONLY — add @swake/web. This SDK is browser-only: put it in a 'use client' provider component mounted in app/layout.tsx (or pages/_app.tsx), and load the widgets with await import('@swake/web/ui') INSIDE the effect. Never import @swake/web/ui at module scope in a server component or a shared layout module — it must not be evaluated during SSR or prerender.
4. INIT EXACTLY ONCE, IDEMPOTENTLY — keep a module-level "let initPromise: Promise<void> | null" and have every caller await the SAME promise. This is required, not stylistic: the SDK exposes no isInitialized(), every method throws "[Swake] SDK not initialized" before init(), and a second init() starts a second offline-queue flush interval without clearing the first. Swake.init() itself is synchronous. PROXY mode: pass any non-empty placeholder as apiKey, point baseUrl at a Route Handler you add at app/api/swake/[...path]/route.ts, which forwards method, path, query and body to the Swake API, and set credentials: 'include'. That backend attaches the real X-API-Key server-side, overrides any client-supplied externalUserId with the session user so identity cannot be spoofed, and must allow X-API-Key and Idempotency-Key through CORS. Set trigger: { hidden: true } if the app will open feedback from its own button instead of the floating one.
5. IDENTITY — after init resolves, call Swake.identify({ userId, email, name, metadata }) on EVERY load that has a session, not just on the login event: identity is held in memory only, so a page refresh loses it. userId must be your stable internal id, and every metadata value must be a string. On logout call Swake.clearIdentity() inside a try/catch — it throws if init never ran. Read the session from your auth library (next-auth's useSession, Clerk, Supabase, whatever is present) and re-identify whenever it resolves — including after a refresh, which is exactly the case a login-event-only wiring misses.
6. POLISH — drive Swake.setTheme() from the app's light/dark toggle (widgets otherwise keep the theme captured at init); gate init/identify by route so an auto-shown survey can never land on login, signup, or onboarding; add Swake.addBreadcrumb() at meaningful domain events so bug reports arrive with context.
7. VERIFY & REPORT — funnel every SDK call through one wrapper with try/catch, then prove it end to end: the trigger opens the form, a real submission appears in the Swake dashboard, and no survey shows on an auth or onboarding route. Report the files you changed, any manual steps left for me, and every assumption you made. Confirm it in a production build (next build && next start), not just dev, so any accidental server-side evaluation of the widget layer surfaces.

Reference

Full API Reference

Click any method for its full signature, parameters, and an example.

MethodDescription
Initialise the SDK. Must be called once before any other method.
Link the current user. Returns a Promise.
Clear user identity and offline queue.
Replace the global UI theme at runtime.
Submit feedback programmatically (with optional customFields). Returns the created Submission.
Fetch the project's custom field definitions for a custom feedback UI.
Open the built-in feedback form widget.
Open the feedback history panel.
Open the feedback form (alias).
Close any open widget (form, history, survey).
Show or hide the trigger FAB.
List the current user's submissions.
Get full submission detail with comments + attachments.
Edit a submission you own.
Unpublish a submission you own (one-way).
Lightweight submission history (id, title, status, type, createdAt).
Get unread notification count.
List notifications with pagination.
Mark a single notification as read.
Mark all notifications as read.
Re-check survey eligibility and show the first match.
Show a specific survey (ignores cooldown).
Register survey-shown event listener.
Register survey-completed event listener.
Register survey-dismissed event listener.
Fetch voting board items.
Cast a vote on a board item.
Remove a vote from a board item.
Add a custom breadcrumb to the trail.
Return a snapshot of the breadcrumb trail.
Clear the breadcrumb ring buffer.
Register plan-limit event listener.
Tear down all listeners, timers, and UI.