Add behavioural analytics to a website or web service
Implement consent-led, metadata-only behavioural analytics, configure access, annotate safe interaction points and consume the Flux APIs.
How Flux works
Flux records controlled interaction metadata only after a visitor grants consent. The browser tag validates each event before sending it to the tenant-scoped collector. The collector validates it again, checks that the browser origin is allowed for the tenant, and stores accepted events for the authorised dashboard.
- Configure your service. A Flux administrator creates or confirms your tenant, allowed web origins and dashboard access.
- Load the browser tag. Every instrumented page supplies the collector endpoint and tenant ID.
- Connect consent. Your consent choice calls
flux('consent', 'granted')orflux('consent', 'revoked'). - Mark safe elements. Use stable, non-identifying
data-flux-keyvalues or send deliberate events. - Verify collection. Check the network response and then the signed-in journey dashboard.
Quick start
This example uses manual consent and manual events. It is the recommended starting point because your service stays in control of consent wording and exactly which interactions are measured.
1. Get your configuration
Ask your Flux workspace administrator for:
- your tenant ID, for example
researchops; - the collector URL, currently
https://flux-behaviour.pages.dev/api/collect; - confirmation that every production, staging and local origin you will use is allow-listed;
- a dashboard account if you need to verify or analyse events.
2. Add the tag inside <head>
<script>
window.flux = window.flux || function () {
(window.flux.q = window.flux.q || []).push(arguments);
};
</script>
<script type="module"
src="https://flux-behaviour.pages.dev/assets/flux/sdk/flux-browser.install.mjs"
data-flux-endpoint="https://flux-behaviour.pages.dev/api/collect"
data-flux-tenant="YOUR_TENANT_ID"></script>
The hosted /assets/flux/* modules are served with Access-Control-Allow-Origin: * so browsers can load the module graph from a publisher website. If you self-host or proxy the modules, return an equivalent CORS header for every imported Flux module, not only the entry file.
3. Connect your consent controls
document.querySelector('#accept-analytics').addEventListener('click', () => {
flux('consent', 'granted');
});
document.querySelector('#reject-analytics').addEventListener('click', () => {
flux('consent', 'revoked');
});
4. Send a safe event
flux('event', 'nav', 'page.loaded', {
role: 'page',
element_key: 'page.application.start'
});
The queue function lets your page call flux() before the module finishes loading. Events sent before consent are dropped immediately; they are not held and replayed later.
Admin and workspace setup
Sign in at the Flux account page with the Google account your workspace administrator has authorised. Account identity is kept separate from analytics visitor and session identifiers.
Report areas in the logged-in interface
All report areas use the same date range, comparison and aggregate-export controls. The default range is the last 30 days; you can also choose 24 hours, 7, 90 or 365 days, all time, or a custom UTC date range of up to 366 days.
| Area | Use it to |
|---|---|
| Overview | Review visitors, journeys, interaction volume, completion, friction and uncertainty for the selected period. |
| Realtime | Check 5- and 30-minute aggregate activity and collector freshness. It never exposes a live view of individuals. |
| Journeys | Inspect recent pseudonymous journeys, controlled narratives and supported behavioural dimensions. |
| Tasks and funnels | Analyse ordered task completion and drop-off. Owners also configure funnels, tasks, steps and exact success events here. |
| Fields | Review privacy-safe field coverage, validation, corrections, dwell and length buckets. Owners configure question groups, complexity, required status and field bindings. |
| Events | Review ranked event, key-event and semantic element aggregates and their period movement. |
| Cohorts | Compare visit maturity, outcome paths and supported journey patterns. Groups smaller than five journeys are suppressed. |
| Model and explainability | See the published service-model version, semantic mappings, configured outcomes, key events and interpretation evidence. |
| Data quality and governance | Review freshness, mapping coverage, versions, export boundaries and known unknowns without hiding incomplete controls. |
Configure Tasks and funnels
- Open Tasks and funnels, then expand Manage task and funnel configuration.
- Create a funnel (transaction), add its tasks, then add the ordered steps a visitor normally completes.
- Bind each step to the exact stable
data-flux-keyemitted by the publisher page. - Add success events using an exact Flux
actionanddata-flux-keypair. A genericflow.submitis not treated as success unless you configure that exact event as an outcome. - Review the dependency summary before deleting a funnel, task or step, then save. Every save publishes a complete immutable next model version.
Configure Fields
- Open Fields, then expand Manage field configuration.
- Add a question group below an ordered step and set publisher-declared complexity from 1 to 7.
- Add fields, mark each required or optional, and bind it to its exact
field.*publisher key. - Save and verify the new model version under Model and explainability.
Install the JavaScript tag
Where to place it
Place the queue snippet and module script once on every page, preferably near the end of <head>. Module scripts are deferred by default, so they do not block HTML parsing. Keeping the queue immediately before the module means consent or event calls made by later page scripts are captured in order.
- Put it in your shared page layout or tag-management template.
- Do not place it inside a component that may render more than once.
- Use one tenant ID for the service represented by that page.
- Keep the script URL and collector endpoint on HTTPS in production.
- Allow the script host in
script-srcand collector inconnect-srcif you use Content Security Policy.
Manual capture (recommended)
Load flux-browser.install.mjs when your application will own consent and explicitly call the events it needs.
<script type="module"
src="https://flux-behaviour.pages.dev/assets/flux/sdk/flux-browser.install.mjs"
data-flux-endpoint="https://flux-behaviour.pages.dev/api/collect"
data-flux-tenant="licence-service"></script>
Automatic capture
Load flux-auto-capture.mjs to add the built-in consent banner and automatically observe page loads, safe controls, tab navigation, form submission and content-free field interaction metadata. Add explicit semantic keys wherever you need stable reporting; otherwise the module may use a structural auto.* fallback.
<script type="module"
src="https://flux-behaviour.pages.dev/assets/flux/sdk/flux-auto-capture.mjs"
data-flux-endpoint="https://flux-behaviour.pages.dev/api/collect"
data-flux-tenant="licence-service"></script>
Content Security Policy example
Content-Security-Policy:
script-src 'self' https://flux-behaviour.pages.dev;
connect-src 'self' https://flux-behaviour.pages.dev;
The inline queue snippet may also require your existing nonce or hash policy. Do not weaken CSP with 'unsafe-inline' solely for Flux.
data-flux-* attributes
Public integration attributes are configuration, not a place to put user content. Attribute values may be sent as event metadata, so use controlled keys only.
| Attribute | Use | Example |
|---|---|---|
data-flux-endpoint | Collector URL read from the module script. Required. | https://flux-behaviour.pages.dev/api/collect |
data-flux-tenant | Tenant/workspace key. Required for the tenant-scoped collector. | licence-service |
data-flux-key | Stable, non-identifying semantic key for a page, field, form or control. Recommended for every reportable element. | field.application.contact-method |
data-flux-role | Overrides the inferred role with one of field, form, control, page, service or environment. | control |
data-flux-page | A controlled page key on <body>, used for automatic page.loaded. | page.application.start |
data-flux-sensitive | Set to true on an input, form or ancestor to exclude its descendant interaction capture. | true |
data-flux-writing-analysis | Set to false to disable content-free on-device UK-English writing metrics for a field. | false |
data-flux-autofocus | Set to true when focus is intentionally assigned by the page, so it is not mistaken for user-originated focus. | true |
data-flux-consent | Used internally by the built-in automatic-capture consent banner. Values are yes or no. Prefer calling the consent command from your own controls. | yes |
Key format
The hosted automatic-capture module accepts data-flux-key values from 1 to 120 characters, but a key must be at least 3 characters to bind it in the publisher service model. Use 3 to 120 characters for reportable service integrations. To keep a key compatible with both collection and the publisher service model, start it with a lowercase letter and use only lowercase letters, numbers, dots, underscores and hyphens. Each separator must be followed by a letter or number. Use type-first keys such as page.*, form.*, button.* and field.*. The event and publisher-model contracts allow up to 160 characters, but using 120 or fewer keeps keys compatible with automatic capture.
Safe
<input
name="contact-method"
data-flux-key="field.application.contact-method">
<button data-flux-key="button.application.continue"
data-flux-role="control">
Continue
</button>Unsafe
<!-- Do not use values, emails or text -->
<input data-flux-key="PERSONAL_EMAIL_ADDRESS">
<button data-flux-key="USER_ENTERED_TEXT">
Continue
</button>Sensitive controls
The automatic-capture module excludes password, email, telephone, one-time-code, payment and authentication-scoped controls. Add data-flux-sensitive="true" when your own component contains sensitive content that cannot be identified safely from standard HTML.
<form data-flux-sensitive="true">
<!-- No descendant interactions are captured -->
</form>
flux('event', ...) calls must never describe interactions with sensitive fields or include entered values, email addresses, telephone numbers, payment details, authentication data or other direct identifiers.Demo-only attributes
data-flux-field, data-flux-help and data-flux-chart belong to the repository demo pages. The hosted SDK does not treat them as public automatic-capture configuration. Use data-flux-key in a service integration.
Consent lifecycle
Consent is a hard collection gate. Until granted, event commands are dropped and identifiers are not created. Revocation stops collection and clears the Flux visitor ID from local storage and session ID from session storage.
// A visitor accepts behavioural analytics
flux('consent', 'granted');
// A visitor later changes their choice (manual capture)
flux('consent', 'revoked');
// When the automatic-capture module is loaded, also update
// its persisted choice so a later page cannot restore consent
localStorage.setItem('flux.behaviour.consent', 'no');
flux('consent', 'revoked');
- Do not call
grantedfrom a page-load default. - With manual capture, persist the visitor's choice in your consent manager, not in event payloads. Automatic capture stores its built-in banner choice under
flux.behaviour.consent; set that value tonobefore callingrevokedfrom an external preference control. - On each new page, restore only a previously recorded valid choice by calling the matching command.
- Offer a way to revoke consent and call
revokedimmediately. - Do not queue pre-consent interactions for later submission.
Events and metadata
Command format
flux('event', EVENT_CLASS, ACTION, {
role: ROLE,
element_key: SAFE_STABLE_KEY,
// optional allow-listed metadata
});
The SDK supplies schema version, pseudonymous visitor and session IDs, tenant ID, consent state, SDK origin and timestamp. Unknown detail fields are stripped. Events that still fail the event contract are dropped.
Event classes
| Class | Use it for | Example action |
|---|---|---|
focus | Focus entry, exit and dwell. | field.focus |
input | Content-free input counts, blur and validation. | field.blur |
nav | Page, step, click and form navigation. | page.loaded |
kbd | Non-content keyboard interaction patterns. | navigation.tab |
clipboard | Copy/paste counts, never clipboard text. | field.paste |
drop / file | Drop or chooser counts and size buckets, never names or contents. | file.selected |
picker | Date, time and controlled picker interactions. | date.opened |
trust | Consent, confirmation and assurance controls. | consent.granted |
assist | Help and guidance use. | assist.help |
a11y | Skip links and interaction-method indicators. | skiplink.used |
env | Controlled network or device context. | network.offline |
Roles
Use one of field, form, control, page, service or environment.
Optional metadata
| Field | Meaning |
|---|---|
duration_ms | Bounded interaction or dwell duration. |
value_length | Character count only, never the value. |
edit_count, key_press_count, backspace_count, paste_count | Bounded interaction counts. Key identity and clipboard content are never recorded. |
dwell_before_input_ms, typing_duration_ms | Bounded time before first edit and active typing duration. |
chars_per_minute, words_per_minute, revisit_count | Derived typing pace and refocus count. |
| Writing counts | word_count, spelling and grammar issue counts, letter-case counts and the fixed en-GB analyzer label. Analysis runs on the device; words and text never leave the page. |
navigation_direction | forward, back, skip or unknown. |
pointer_type | mouse, pen, touch, keyboard or unknown. |
reason | A contract-controlled reason: empty_field, control_nonresponsive, validation_error, help_requested or unknown. |
file_count, file_size_bucket | File count and none/small/medium/large/unknown bucket only. |
Example: field completion
flux('event', 'input', 'field.blur', {
role: 'field',
element_key: 'field.application.contact-method',
duration_ms: 4200,
edit_count: 1,
value_length: 5,
pointer_type: 'keyboard'
});
Never add a value, label text, email, field name containing personal data, clipboard content, file name or raw error message.
Single-page applications
Install the tag once in the application shell. Route changes do not reload it, so send a controlled page event after your router confirms navigation.
router.afterEach((to, from) => {
flux('event', 'nav', 'page.loaded', {
role: 'page',
element_key: to.meta.fluxKey,
navigation_direction: 'forward'
});
});
Define to.meta.fluxKey in route configuration. Do not derive it from path parameters, search terms, account IDs or user-entered URLs. If components mount repeatedly, make sure handlers are removed on unmount so one interaction produces one event.
HTTP APIs
The current API is deliberately small. The browser collector is the public integration boundary. Authentication and dashboard routes support the Flux web interface; they are documented here so integrators understand the complete request flow, but they are not a general-purpose reporting API.
Base URL: https://flux-behaviour.pages.dev
POST /api/collect
Accepts one schema version 1.2.0 event per request. Prefer the SDK: it creates identifiers after consent, strips unknown fields and validates before transport.
const response = await fetch(
'https://flux-behaviour.pages.dev/api/collect',
{
method: 'POST',
credentials: 'omit',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
schema_version: '1.2.0',
session_id: 'session-7fce44fdaef1434398c07c48',
visitor_id: 'visitor-7a06bb4a92fa49e5a23877ad',
tenant_id: 'licence-service',
consent: 'yes',
origin: 'sdk',
event_class: 'nav',
action: 'page.loaded',
role: 'page',
element_key: 'page.application.start',
timestamp_ms: Date.now()
})
}
);
if (!response.ok) {
console.warn('Flux event was not accepted', response.status);
}
Success: 202 Accepted
{
"ok": true,
"accepted": true,
"returning_visitor": false
}
| Status | Meaning |
|---|---|
202 | Event validated and was accepted for storage. |
400 invalid_event | JSON was malformed or did not satisfy the event contract. |
403 origin_not_allowed | Tenant is unknown or the browser Origin is not configured. |
503 storage_unavailable | The service does not have its D1 binding available. |
/api/collect
Browsers send a CORS preflight for cross-origin collection. An allowed origin receives 204 No Content with POST, OPTIONS and content-type allowed. You normally do not call this yourself.
GET /api/auth/google/start
Starts the interactive Google sign-in flow. Navigate the browser to this route; do not call it with fetch(). It redirects to Google and sets a short-lived, signed, secure state cookie. It returns 503 google_sign_in_unconfigured until the protected OAuth configuration is present.
GET /api/auth/google/callback
OAuth callback used by Google. The service validates state, resolves the verified Google account to a Flux account and sets the signed flux_session cookie. Applications must not construct or call this URL directly.
GET /api/service-model/:tenant
Returns the currently published immutable service model, manifest hash and your tenant role. This requires a valid Flux session and tenant membership.
const response = await fetch('/api/service-model/licence-service', {
credentials: 'include'
});
const { model, manifest_hash, role } = await response.json();
PUT /api/service-model/:tenant
Publishes a complete next service-model version. The authenticated account must be a tenant owner, the JSON tenant_id must match the path, and an existing version cannot be overwritten. Prefer the dashboard editors unless you manage models as governed configuration.
const response = await fetch('/api/service-model/licence-service', {
method: 'PUT',
credentials: 'include',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(nextCompleteModel)
});
if (response.status === 409) {
// Read the latest model, reconcile, then publish a new version.
}
201 publishes the version; 400 means the complete model is invalid; 403 means the account is not an owner; and 409 prevents version overwrite.
GET /api/dashboard/researchops
Returns the authenticated ResearchOps dashboard dataset: overview, trend, realtime, journeys, cohorts, lifecycle, service-model, event, entity, funnel, field, comparison, uncertainty and governance reports. It requires the secure Flux session cookie and is tenant-specific, not a general-purpose public reporting API.
const response = await fetch('/api/dashboard/researchops', {
credentials: 'include'
});
if (response.status === 401) {
location.assign('/account/');
}
const data = await response.json();
401 unauthorised means the session is missing or expired. 403 forbidden means the signed-in account does not have ResearchOps access.
Use range=24h|7d|30d|90d|1y|all, or range=custom&start=YYYY-MM-DD&end=YYYY-MM-DD. Use compare=period|visit_maturity|outcome|task|interaction_mode. The default is range=30d&compare=period.
GET /api/dashboard/researchops/session/:sessionId
Loads one authorised pseudonymous journey and its chronological, controlled event presentation for intentional journey inspection. Use the session IDs returned by the dashboard endpoint; do not expose this route in an unauthenticated client.
GET /api/dashboard/researchops/export.csv
Downloads an authenticated aggregate CSV. Set report to overview, events, key_events, elements, entities, funnels, fields or comparison. Range and comparison parameters match the dashboard endpoint.
const url = new URL(
'/api/dashboard/researchops/export.csv',
location.origin
);
url.search = new URLSearchParams({
report: 'funnels',
range: '30d',
compare: 'period'
});
location.assign(url);
Exports are capped at 5,000 aggregate metric rows, neutralise spreadsheet formula prefixes and never include raw events, narratives, metadata, visitor IDs, session IDs or entered values. Comparison groups smaller than five journeys are suppressed.
Versioning and limits
- The event contract version is in every event as
schema_version; the current value is1.2.0. - Breaking event changes require a new schema version.
- Send one event per collector request. Batch payloads are not supported.
- The SDK does not retry failed events or buffer them offline.
- Authenticated application APIs use the secure Flux session cookie. Do not copy or construct that cookie.
- No public API-key authentication, pagination, webhooks or tenant-generic reporting API is available yet.
Security and privacy requirements
- Collect only after explicit consentTreat revocation as immediate and clear identifiers.
- Use stable controlled keysNever use labels, URL parameters, account data or user-entered text as
element_key. - Do not send credentials to the collectorThe browser transport uses
credentials: 'omit'. Do not add cookies or authorisation headers. - Keep secrets server-sideOAuth client secrets and Flux auth secrets belong in the platform secret manager, never HTML or JavaScript.
- Complete service governanceAgree purpose, retention, deletion, accessibility, incident handling and DPIA before live use.
Test your installation
- Open a staging page with browser developer tools.
- Before consent, interact with keyed controls and confirm there are no requests to
/api/collect. - Grant consent and trigger one deliberate event.
- In the Network panel, confirm a
POSTto the configured collector returns202. - Inspect the request JSON. Confirm it contains controlled keys and counts only—no typed values, labels, email addresses, URL query values, clipboard text or file names.
- Revoke consent, trigger another interaction and confirm no request is sent.
- Confirm
flux.behaviour.visitor_idandflux.behaviour.session_idare absent after revocation. - Sign in to the Flux dashboard and confirm the accepted event appears in the recent journey narrative.
- Repeat with keyboard-only navigation and at supported responsive widths.
For a local checkout of this repository, run npm run demo:serve and open http://localhost:4321/developers/. The repository test suite uses npm test.
Troubleshooting
No collector request appears
The collector returns 403
Origin header with the origins registered for the tenant. Scheme, host and port must match. An origin must not include a path.The collector returns 400
1.2.0 and supply every required field. Prefer the SDK, which builds and validates the envelope.Events appear more than once
Google sign-in is unavailable
google_sign_in_unconfigured until the OAuth client and protected secrets are configured. If sign-in succeeds but access is forbidden, ask the workspace administrator to link the verified Google account to the tenant.An event is missing from the dashboard
202. The current product does not provide delivery-loss metrics.Implementation checklist
- Tenant and all required origins confirmed
- Dashboard access confirmed for named team members
- Tag installed once in the shared layout
- CSP updated without weakening the policy
- Consent grant, restore and revoke flows connected
- Only stable, non-identifying keys used
- Pre-consent, post-consent and revocation tested
- Collector response and dashboard narrative verified
- Privacy, retention, accessibility and release approval recorded