First-party measurement and consent

Embed
Preview of the card
<a href="https://forumulate.com/guides/first-party-measurement"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/c9b597cb-d7eb-4034-acea-4d1e8c0061ad/social-card.svg?v=3e7ae33eec8b" alt="First-party measurement and consent" width="600" height="315"></a>

The analytics tags you add (Google Analytics, Plausible, PostHog and the rest) are yours, and your consent manager governs them by category. This page is about the other kind: the counters Forumulate itself keeps so that your console can show views, unread searches, form drop-off and A/B results. They are first-party, they never leave our service, and they are now gated by the same consent as your own tags.

What we measure, and what each records

Every one of these is sent from the reader page to our API. None sets a cookie of its own, none is shared with a third party, and none identifies a person: where a count is "per reader", the reader is a keyed hash of the address and browser that only the server can compute, never the address itself.

Measurement

Sent to

What is stored

Browser storage

Docs page view

POST /public/orgs/{org}/spaces/{space}/pages/{page}/view

One view per reader per page per day, with the referring site

none

Forum topic view

POST …/forum/t/{slug}/view

One view per reader per topic per day

sessionStorage fv:… (once per tab)

Blog post, release, roadmap item view

POST …/engagement (kind: view)

One reader per post per day; the post's view count

none

Call-to-action click

POST …/engagement (kind: cta_click)

One click per reader per release per day, with the link

none

Event page view and registration

POST …/events/{handle}/insights/view

A per-day counter by utm_source / utm_medium / utm_campaign (or referring site)

sessionStorage fx-event-viewed:…, fx-event-utm (this tab only)

Form progress

POST /public/orgs/{org}/form-flow/…/progress

A form "pass": viewed, started, last question touched, how many answered; the answers only if you keep partial answers

sessionStorage fm_sess:… (a random pass id, this tab only)

Docs search

POST …/search-events

The query and how many results it found (the "searches with no results" report)

none

FAQ search

POST …/faq/search-events

The settled query and how many questions it matched

none

A/B exposure and goal

POST …/pages/{page}/exposure, …/goal

Which arm a reader saw; which goal they reached, credited to that arm

cookie fx_ab (the arms, 90 days)

Course landing variant

POST …/course/landing-variants/{id}/view, …/conversion

Views and conversions per variant

localStorage fx-landing-variant:…

Course affiliate click

POST …/course-affiliates/{code}/click

One click per referral code

localStorage fx-course-ref:… (the code, 30 days)

Member read time

POST /orgs/{org}/spaces/{space}/forum/{topic}/read-time

Seconds a signed-in member had the topic open

none

Changelog widget open and click

POST …/changelog/widget/hit

One open or click per reader per day

none

A few things readers do are not measurement and are not gated: rating a page ("Was this helpful?"), leaving a comment or an NPS score, asking a question, voting, following. Those are things the reader chose to send you.

Server renders never count. A blog post, a roadmap item and a docs search used to be counted inside the API read that rendered them; the reader now marks those reads (X-Forumulate-Measure: client, or ?measure=client on the search) and counts them through the gate instead. A caller that does not mark its read (the SDK, an agent) is counted as before.

There is one gate on the reader, and every row in the table above goes through it, including the browser storage in the last column.

  • A consent manager is active on the space (our hosted banner, or any provider you configured under Cookies & consent): a measurement runs only once the reader has granted analytics. Before they decide it waits on the page. A grant sends it; a refusal drops it, and so does leaving the page. Nothing is sent, and nothing is written to the browser, before a decision.

  • No consent manager (the space is unconfigured, or set to none): what happens is the firstPartyAnalytics setting below. A reader who refused analytics in Cookie settings is still not counted.

The decision is read from Google Consent Mode v2 (analytics_storage), which every supported provider sends, so the gate does not depend on any one vendor. A provider that sends no Consent Mode signal is treated as "not yet decided", and nothing is counted.

The changelog widget runs on your own product, not on our reader. There it follows the host page's Consent Mode when the page has one (a default of denied counts as a no until the reader answers), and the setting below when it has none.

The setting: firstPartyAnalytics

always, with-consent or off. Set it on the organization (PUT /orgs/{orgId}/consent) and override it on a space (PUT /orgs/{orgId}/spaces/{spaceId}/consent, with mode: inherit or mode: override). A space's value wins, then the organization's, then the default.

Value

No consent manager

Consent manager active

always (default)

Counted, unless the reader refused in Cookie settings

Counted only after a grant

with-consent

Counted only after a grant in Cookie settings

Counted only after a grant

off

Never counted

Never counted

The default is always because of what these counters are: aggregates kept on our side, deduplicated by a hash only the server can compute, never a cookie and never a lasting identifier on the reader's device. That is the kind of audience measurement European regulators exempt from prior consent, and it is what every space did before this setting existed. If your counsel reads it differently, set with-consent or off. An active consent manager gates measurement whatever this says, so the setting only decides the case where no one is asked.

The public resolve, GET /public/orgs/{orgId}/spaces/{spaceId}/consent, always answers with the resolved value, so you can check what a space will do:

{ "provider": "hosted", "scriptSources": [], "firstPartyAnalytics": "always" }

In the API

Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.

Operation

Request

What it does

Shortcuts

ConsentPublic_resolved

GET /public/orgs/{orgId}/spaces/{spaceId}/consent

Get the consent in a space (public reader)

client.privacy.publicConsent.resolved()

ConsentPublic_record

POST /public/orgs/{orgId}/spaces/{spaceId}/consent/receipts

Create a receipt in a space

client.privacy.publicConsent.record()

The Forumulate teamUpdated