SDK & frameworks

SvelteKit

Call initFlusterduck once in your root layout behind a browser guard, then import the helpers you need.


No store, no plugin object. @flusterduck/svelte exports individual functions. Call initFlusterduck once in your root layout behind a browser guard, then import signal, track, identify, setConsent, and optOut wherever you need them.

Install

bash
pnpm add flusterduck @flusterduck/svelte

Setup

svelte
<!-- src/routes/+layout.svelte -->
<script>
  import { initFlusterduck } from '@flusterduck/svelte'
  import { browser } from '$app/environment'

  if (browser) {
    initFlusterduck({
      key: import.meta.env.PUBLIC_FLUSTERDUCK_KEY,
    })
  }
</script>

<slot />
bash
# .env
PUBLIC_FLUSTERDUCK_KEY=fd_pub_xxxxxxxxxxxx

The browser guard isn't optional. SvelteKit runs your layout on the server during SSR, and the SDK is browser-only. Remove the guard and you get a runtime error on first load.

initFlusterduck

ts
initFlusterduck({
  key: import.meta.env.PUBLIC_FLUSTERDUCK_KEY,
  environment: 'production',
  sampleRate: 1.0,
})

Calling it a second time is a no-op. A module-level initialized flag blocks re-entry, so hot reloads and layout remounts don't cause duplicate initialization.

Options

OptionTypeDefaultDescription
keystring-Required. Your fd_pub_ key.
environmentstring-"production", "staging", "development"
sampleRatenumber1.00.5 tracks half of sessions.
domMode'off' | 'metadata' | 'snapshot''off''metadata' captures element attributes. 'snapshot' adds layout and computed styles.
cookielessbooleanfalseMemory-only session IDs instead of cookies.
respectDoNotTrackbooleantrueHonor navigator.doNotTrack. Most teams pass false: Flusterduck captures no PII, and the default silently drops 30-50% of visitors on Firefox, Brave, and other privacy-focused browsers.
ignoreElementsstring[][]CSS selectors to suppress signals on.
ignorePagesstring[][]Page paths to skip.
segmentRecord<string, string>-Static tags on every event.
debugbooleanfalseVerbose console logging.
batchIntervalnumber-Milliseconds between flushes. Default flushes on idle and before unload.
elementImpressionSelectorsstring[]-CSS selectors to emit an impression signal when they enter the viewport.

destroyFlusterduck() tears down the SDK and resets the initialized flag. Use it in tests or if you're dynamically switching sites.

Exports

ExportSignatureDescription
initFlusterduck(config: Config) => voidInitialize the SDK. Call once.
destroyFlusterduck() => voidTear down and reset.
signal(name: string, data?: { element?: string; metadata?: Record<string, unknown>; weight?: number }) => voidEmit a friction signal manually.
track(name: string, metadata?: Record<string, unknown>) => voidTrack a business event.
identify(segment: Record<string, string>) => voidTag the session with user properties.
setConsent(consented: boolean) => voidPause or resume collection.
optOut() => voidStop collection permanently for this session.

Tracking in components

svelte
<script lang="ts">
  import { track } from '@flusterduck/svelte'

  export let planId: string
  export let priceCents: number
</script>

<button on:click={() => track('plan_intent', { plan_id: planId, price_cents: priceCents })}>
  Get started
</button>
svelte
<script lang="ts">
  import { signal } from '@flusterduck/svelte'

  let currentStep = 0
</script>

<div on:mouseleave={() => signal('task_abandonment', { metadata: { step: currentStep } })}>
  <!-- wizard step content -->
</div>

Importing from flusterduck directly works just as well. Both go through the same initialized SDK instance.

svelte
<!-- src/lib/components/ConsentBanner.svelte -->
<script lang="ts">
  import { onMount } from 'svelte'
  import { setConsent } from '@flusterduck/svelte'

  let visible = false

  onMount(() => {
    const stored = localStorage.getItem('fd_consent')
    if (stored === null) {
      setConsent(false)
      visible = true
    } else {
      setConsent(stored === 'true')
    }
  })

  function accept() {
    localStorage.setItem('fd_consent', 'true')
    setConsent(true)
    visible = false
  }

  function decline() {
    localStorage.setItem('fd_consent', 'false')
    setConsent(false)
    visible = false
  }
</script>

{#if visible}
  <div>
    <p>We use behavioral analytics to improve this product.</p>
    <button on:click={accept}>Accept</button>
    <button on:click={decline}>Decline</button>
  </div>
{/if}

The setConsent(false) call before showing the banner isn't a pause: it flushes anything the SDK already buffered since initFlusterduck ran, sends it, and tears the SDK down. setConsent(true) on accept starts a fresh session by calling init() again, it doesn't resume whatever was flushed on decline. If zero pre-consent collection is a hard requirement, don't call initFlusterduck until you have consent, rather than initializing and declining immediately after.

Identifying users

svelte
<!-- src/lib/components/IdentifyUser.svelte -->
<script lang="ts">
  import { onMount } from 'svelte'
  import { identify } from '@flusterduck/svelte'

  export let userId: string | undefined = undefined
  export let orgId: string | undefined = undefined
  export let plan: string | undefined = undefined

  onMount(() => {
    if (!userId) return
    identify({ user_id: userId, org_id: orgId ?? '', plan: plan ?? 'trial' })
  })
</script>

Mount this inside whatever layout has access to your auth state. Pass an opaque internal ID, not an email or display name.

Deploy tagging

svelte
<!-- src/routes/+layout.svelte -->
<script>
  import { initFlusterduck } from '@flusterduck/svelte'
  import { browser } from '$app/environment'

  if (browser) {
    initFlusterduck({
      key: import.meta.env.PUBLIC_FLUSTERDUCK_KEY,
      segment: {
        app_version: import.meta.env.PUBLIC_APP_VERSION ?? 'unknown',
      },
    })
  }
</script>

Gotchas

The browser guard is required, not optional. If you skip it, SvelteKit's SSR pass throws on first request. The error won't always be obvious. It can manifest as a hydration mismatch rather than an explicit SDK error.

There's no Svelte store. If you expected $flusterduck.track(...), that's not how this works. The package exports plain functions. Import them directly where you need them.

initFlusterduck in a +layout.svelte runs on both client and server unless guarded. The browser import from $app/environment is the canonical way to guard it. Don't use typeof window !== 'undefined'. It works but it's not idiomatic SvelteKit.

Hot module replacement. During development, Vite's HMR can re-execute your layout script. The initialized flag means initFlusterduck won't re-run, which is the right behavior. You don't want a new session mid-development.

TypeScript

Config is exported and typed. signal()'s second argument isn't, it's an inline shape, not a named export, so pass the object literal directly rather than importing a SignalData type that doesn't exist:

ts
import type { Config } from 'flusterduck'
import { initFlusterduck, signal } from '@flusterduck/svelte'

const config: Config = {
  key: import.meta.env.PUBLIC_FLUSTERDUCK_KEY,
  environment: 'production',
}
initFlusterduck(config)

signal('dead_click', {
  element: '[data-action="publish"]',
  metadata: { blocked_by: 'missing_required_fields', field_count: 3 },
})