# Attribution and hidden fields

Every response records where the person came from, so a form linked from twenty pages tells you which page sent them, and a campaign isn't lost when the visitor browses before filling it in:

| | Stored as | Example |
| --- | --- | --- |
| The page with the form (origin and path, no query string) | `meta.pageUrl` | `https://acme.com/demo` |
| The page on your site that led to the form, or the other site that linked straight to it | `meta.referrer` | `/pricing`, or `https://news.ycombinator.com` |
| The first page of the visit, when it isn't the form's page | `meta.landingPage` | `/blog/launch` |
| The other site the visit arrived from, when the form's page wasn't the first | `meta.entryReferrer` | `https://www.google.com` |
| Campaign tags from the form's page, else from the landing page | `meta.utm` | `{ "source": "google", "medium": "cpc" }` |

It shows on each response (Came from, Landing page, Visit came from), in Slack and email notifications ("From /pricing · via google / cpc · landed on /blog/launch"), in Insights (Where people come from, Pages that led here), in the CSV and API exports (`page_url`, `referrer`, `landing_page`, `entry_referrer`, `utm_*`), and in webhooks and database rows (`meta`).

## Record it on every page (optional)

A page with a form records it by itself. To also know the pages a visitor saw *before* reaching the form (which page sent them, where the visit started), add `KibbleAttribution` once to the root layout. It's optional: a choice for the site owner, since it runs on every page. It takes no props and renders nothing, so a Next.js server layout can render it:

```tsx
// app/layout.tsx
import { KibbleAttribution } from 'kibbleforms';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <KibbleAttribution />
        {children}
      </body>
    </html>
  );
}
```

Outside React, call `startAttribution()` from `kibbleforms/client` once per page load. Without either, responses still carry the form's own page, the site that linked straight to it and its campaign tags.

## Privacy

- Other sites are recorded as their origin only (`https://www.google.com`), never their path or query, which can hold search terms or tokens. Pages on your site are recorded as paths, without query strings. Kibble's API trims whatever a client sends to this, including older versions that send the full `document.referrer`.
- The visit record lives in the visitor's own `sessionStorage` for that tab, until the tab closes. It's sent only with a submission. No cookies, nothing shared across sites.
- Turn it off with `createClient({ projectId, attribution: false })` in `kibble/client.ts`: responses then carry only what the form's page knows.
- **Consent.** In the EU and UK, keeping data in a visitor's browser and sending it later can need consent under the ePrivacy rules, even without cookies, when it isn't strictly necessary for the form. The site owner decides. If your site asks for consent for analytics, treat attribution the same way: pass `attribution: false` until the visitor agrees, or leave it off. Mention Kibble in your privacy notice either way ([what Kibble stores](https://kibbleforms.com/legal/privacy#3-responses-we-store-for-customers)).

## Hidden fields

A `hidden` field isn't shown; its value comes from the URL, like Typeform's hidden fields. Link to `/demo?plan=pro&partner=acme` and:

```ts
fields: [
  { key: 'plan', type: 'hidden', label: 'Plan' },                                    // ?plan=…
  { key: 'partner', type: 'hidden', label: 'Partner', prefill: { query: 'partner' } }, // any parameter name
  { key: 'source', type: 'hidden', label: 'Source', defaultValue: 'website' },         // a fixed value
]
```

A hidden field reads the parameter named after its key, or `prefill.query`. When the form's page doesn't have the parameter, it reads it from the landing page of the visit, so `acme.com/?plan=pro` → `/pricing` → `/demo` still fills `plan` (with `KibbleAttribution` in the layout). `prefill.query` works on visible fields too, and `initialValues` on `<Form>` sets values from code (it wins over the URL). Hidden values are stored and delivered like any answer.
