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

pnpm add flusterduck @flusterduck/svelte

Setup

<!-- 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 />
# .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

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.
respectDoNotTrackbooleanfalseHonor navigator.doNotTrack.
ignoreElementsstring[][]CSS selectors to suppress signals on.
ignorePagesstring[][]Page paths to skip.
segmentRecord<string, string>-Static tags on every event.
debugbooleanfalseVerbose console logging.

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?: SignalData) => 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

<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>
<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.

<!-- 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 pauses collection immediately. The user's choice then either resumes it or confirms the pause.

Identifying users

<!-- 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

<!-- 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

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

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

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