SDK & frameworks

React

Wrap your app root with FlusterduckProvider and signal detection starts across the whole tree.


Wrap your app root with FlusterduckProvider. Signal detection starts immediately across the entire tree. That's the whole setup for most apps.

Reach for useFlusterduck when initialization needs to be conditional: consent flows, feature flags, auth-gated tracking. Don't use both patterns in the same app.

Install

pnpm add flusterduck @flusterduck/react

Setup

// src/main.tsx
import { createRoot } from 'react-dom/client'
import { FlusterduckProvider } from '@flusterduck/react'
import { App } from './App'

createRoot(document.getElementById('root')!).render(
  <FlusterduckProvider apiKey={import.meta.env.VITE_FLUSTERDUCK_KEY}>
    <App />
  </FlusterduckProvider>
)
# .env
VITE_FLUSTERDUCK_KEY=fd_pub_xxxxxxxxxxxx

Note apiKey, not key. React reserves the key prop for reconciliation. It gets stripped before it reaches the component. apiKey maps internally to key in the SDK config.

FlusterduckProvider

Props

PropTypeDefaultDescription
apiKeystring-Required. Your fd_pub_ key.
environmentstring-"production", "staging", "development"
sampleRatenumber1.00.1 tracks 10% of sessions. Useful for high-traffic apps where full coverage is expensive.
domMode`'off' \'metadata' \'snapshot'`'off''metadata' captures element attributes with each signal. 'snapshot' adds computed layout and styles.
cookielessbooleanfalseMemory-only session IDs instead of cookies.
respectDoNotTrackbooleanfalseHonor navigator.doNotTrack.
ignoreElementsstring[][]CSS selectors to suppress signals on. Good for admin toolbars or debug overlays.
ignorePagesstring[][]Page paths to skip entirely.
segmentRecord<string, string>-Static tags on every event: app version, experiment variant, user cohort.
debugbooleanfalseLogs every signal and flush to the console.
enabledbooleantruefalse skips initialization. Flip to true and it initializes then.

useFlusterduck hook

Initializes the SDK and returns tracking methods. Use this instead of FlusterduckProvider when the init decision isn't static.

// src/App.tsx
import { useFlusterduck } from '@flusterduck/react'

export function App() {
  const { signal, track, identify, setConsent, optOut } = useFlusterduck({
    key: import.meta.env.VITE_FLUSTERDUCK_KEY,
  })

  return <Router />
}

Options are identical to FlusterduckProvider props, but uses key instead of apiKey (no JSX involved, no prop stripping).

Return value

MethodDescription
signal(name, data?)Emit a friction signal manually.
track(name, metadata?)Track a business event.
identify(segment)Tag the session with user properties.
setConsent(consented)Pause or resume collection.
optOut()Stop collection permanently for this session.

Tracking in components

Initialize once at the root. After that, import from flusterduck directly in any component. No hook call needed:

import { signal } from 'flusterduck'

export function MultiStepForm({ step }: { step: number }) {
  return (
    <div onMouseLeave={() => signal('task_abandonment', { metadata: { step } })}>
      {/* form content */}
    </div>
  )
}
import { track } from 'flusterduck'

export function PublishButton({ docId }: { docId: string }) {
  return (
    <button onClick={() => track('document_published', { doc_id: docId })}>
      Publish
    </button>
  )
}

Core SDK functions buffer events until the SDK initializes. Calling them before the provider mounts is safe.

Conditional initialization

enabled controls whether the SDK initializes at all. Set it to false and nothing runs. Flip it to true and the SDK initializes at that point.

export function Root() {
  const [consented, setConsented] = useState(false)

  return (
    <FlusterduckProvider
      apiKey={import.meta.env.VITE_FLUSTERDUCK_KEY}
      enabled={consented}
    >
      <App onConsentChange={setConsented} />
    </FlusterduckProvider>
  )
}

enabled: false skips initialization entirely. No session, no buffering, nothing recorded. setConsent(false) initializes but pauses collection. Anonymous session data accumulates until consent flips. Use enabled: false if your legal requirement is zero data before consent. Use setConsent(false) if anonymous pre-consent analytics are acceptable and you want to resume without a full re-init.

import { setConsent } from 'flusterduck'

export function ConsentBanner({ onDecide }: { onDecide: (accepted: boolean) => void }) {
  return (
    <div>
      <p>We use behavioral analytics to improve this product.</p>
      <button onClick={() => { setConsent(true); onDecide(true) }}>Accept</button>
      <button onClick={() => { setConsent(false); onDecide(false) }}>Decline</button>
    </div>
  )
}

setConsent(false) pauses immediately. setConsent(true) resumes. The consent state doesn't persist across reloads. The SDK initializes fresh each session, so you'll re-apply it on mount from whatever storage you use.

Identifying users

import { useEffect } from 'react'
import { identify } from 'flusterduck'

export function IdentityBridge({ userId, plan }: { userId?: string; plan?: string }) {
  useEffect(() => {
    if (!userId) return
    identify({ user_id: userId, plan: plan ?? 'trial' })
  }, [userId, plan])

  return null
}

Render this inside your auth provider where userId is reliably available. Pass an opaque internal ID, not an email, name, or anything PII.

Deploy tagging

<FlusterduckProvider
  apiKey={import.meta.env.VITE_FLUSTERDUCK_KEY}
  segment={{ app_version: import.meta.env.VITE_APP_VERSION ?? 'unknown' }}
>
  <App />
</FlusterduckProvider>

Every session gets tagged with the version that was running when they landed. Confusion score comparisons in the dashboard then map cleanly to your deploy history.

React StrictMode

StrictMode double-invokes effects in development to surface side effects. The useFlusterduck hook handles this correctly. A useRef flag prevents double initialization, and the cleanup function cancels any in-flight dynamic import. You won't get duplicate init() calls.

Gotchas

Use apiKey, not key. React strips the key prop from elements before they reach the component. <FlusterduckProvider key="fd_pub_..."> silently passes nothing. This is by design. The fix is already baked in, but if you see the SDK not initializing, check that you're passing apiKey.

Don't call useFlusterduck in child components. There's no reason to. Initialize once at the root, import directly from flusterduck everywhere else. Calling useFlusterduck in multiple components won't double-initialize, but it creates unnecessary hooks.

FlusterduckProvider and useFlusterduck both call init() internally. Don't wrap your app with FlusterduckProvider AND call useFlusterduck somewhere in the tree. Pick one approach.

TypeScript

import type { SignalData } from 'flusterduck'

const data: SignalData = {
  element: '[data-role="submit"]',
  metadata: { validation_errors: ['email_invalid', 'name_required'] },
}
signal('form_error', data)