Flux Behaviour developer documentation

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.

Flux is service-improvement evidence. It must not be used to judge a person or make eligibility, enforcement, fraud or other high-stakes decisions.

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.

  1. Configure your service. A Flux administrator creates or confirms your tenant, allowed web origins and dashboard access.
  2. Load the browser tag. Every instrumented page supplies the collector endpoint and tenant ID.
  3. Connect consent. Your consent choice calls flux('consent', 'granted') or flux('consent', 'revoked').
  4. Mark safe elements. Use stable, non-identifying data-flux-key values or send deliberate events.
  5. Verify collection. Check the network response and then the signed-in journey dashboard.

Current product status

Flux is pre-release. Collection and the ResearchOps dashboard exist, but production rate limiting, retention and deletion policy, DPIA and full accessibility evidence remain release blockers.

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.

Flux dashboard report areas
AreaUse it to
OverviewReview visitors, journeys, interaction volume, completion, friction and uncertainty for the selected period.
RealtimeCheck 5- and 30-minute aggregate activity and collector freshness. It never exposes a live view of individuals.
JourneysInspect recent pseudonymous journeys, controlled narratives and supported behavioural dimensions.
Tasks and funnelsAnalyse ordered task completion and drop-off. Owners also configure funnels, tasks, steps and exact success events here.
FieldsReview privacy-safe field coverage, validation, corrections, dwell and length buckets. Owners configure question groups, complexity, required status and field bindings.
EventsReview ranked event, key-event and semantic element aggregates and their period movement.
CohortsCompare visit maturity, outcome paths and supported journey patterns. Groups smaller than five journeys are suppressed.
Model and explainabilitySee the published service-model version, semantic mappings, configured outcomes, key events and interpretation evidence.
Data quality and governanceReview freshness, mapping coverage, versions, export boundaries and known unknowns without hiding incomplete controls.

Configure Tasks and funnels

  1. Open Tasks and funnels, then expand Manage task and funnel configuration.
  2. Create a funnel (transaction), add its tasks, then add the ordered steps a visitor normally completes.
  3. Bind each step to the exact stable data-flux-key emitted by the publisher page.
  4. Add success events using an exact Flux action and data-flux-key pair. A generic flow.submit is not treated as success unless you configure that exact event as an outcome.
  5. Review the dependency summary before deleting a funnel, task or step, then save. Every save publishes a complete immutable next model version.

Configure Fields

  1. Open Fields, then expand Manage field configuration.
  2. Add a question group below an ordered step and set publisher-declared complexity from 1 to 7.
  3. Add fields, mark each required or optional, and bind it to its exact field.* publisher key.
  4. Save and verify the new model version under Model and explainability.
Tenant owners can edit configuration. Viewers can inspect the published structure and reports but cannot publish changes. Workspace creation, allowed origins and membership remain provisioned by the Flux operator in this pre-release service.

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-src and collector in connect-src if 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>
Review the built-in banner wording and consent storage with your privacy team before using automatic capture. Use manual capture when your service already has a consent manager.

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.

Public attributes
AttributeUseExample
data-flux-endpointCollector URL read from the module script. Required.https://flux-behaviour.pages.dev/api/collect
data-flux-tenantTenant/workspace key. Required for the tenant-scoped collector.licence-service
data-flux-keyStable, non-identifying semantic key for a page, field, form or control. Recommended for every reportable element.field.application.contact-method
data-flux-roleOverrides the inferred role with one of field, form, control, page, service or environment.control
data-flux-pageA controlled page key on <body>, used for automatic page.loaded.page.application.start
data-flux-sensitiveSet to true on an input, form or ancestor to exclude its descendant interaction capture.true
data-flux-writing-analysisSet to false to disable content-free on-device UK-English writing metrics for a field.false
data-flux-autofocusSet to true when focus is intentionally assigned by the page, so it is not mistaken for user-originated focus.true
data-flux-consentUsed 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>
WarningThese exclusions apply only to automatic capture. Manual 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.

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

ClassUse it forExample action
focusFocus entry, exit and dwell.field.focus
inputContent-free input counts, blur and validation.field.blur
navPage, step, click and form navigation.page.loaded
kbdNon-content keyboard interaction patterns.navigation.tab
clipboardCopy/paste counts, never clipboard text.field.paste
drop / fileDrop or chooser counts and size buckets, never names or contents.file.selected
pickerDate, time and controlled picker interactions.date.opened
trustConsent, confirmation and assurance controls.consent.granted
assistHelp and guidance use.assist.help
a11ySkip links and interaction-method indicators.skiplink.used
envControlled network or device context.network.offline

Roles

Use one of field, form, control, page, service or environment.

Optional metadata

FieldMeaning
duration_msBounded interaction or dwell duration.
value_lengthCharacter count only, never the value.
edit_count, key_press_count, backspace_count, paste_countBounded interaction counts. Key identity and clipboard content are never recorded.
dwell_before_input_ms, typing_duration_msBounded time before first edit and active typing duration.
chars_per_minute, words_per_minute, revisit_countDerived typing pace and refocus count.
Writing countsword_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_directionforward, back, skip or unknown.
pointer_typemouse, pen, touch, keyboard or unknown.
reasonA contract-controlled reason: empty_field, control_nonresponsive, validation_error, help_requested or unknown.
file_count, file_size_bucketFile 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
}
Collector responses
StatusMeaning
202Event validated and was accepted for storage.
400 invalid_eventJSON was malformed or did not satisfy the event contract.
403 origin_not_allowedTenant is unknown or the browser Origin is not configured.
503 storage_unavailableThe service does not have its D1 binding available.

OPTIONS /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 is 1.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 consent
    Treat revocation as immediate and clear identifiers.
  • Use stable controlled keys
    Never use labels, URL parameters, account data or user-entered text as element_key.
  • Do not send credentials to the collector
    The browser transport uses credentials: 'omit'. Do not add cookies or authorisation headers.
  • Keep secrets server-side
    OAuth client secrets and Flux auth secrets belong in the platform secret manager, never HTML or JavaScript.
  • Complete service governance
    Agree purpose, retention, deletion, accessibility, incident handling and DPIA before live use.

Test your installation

  1. Open a staging page with browser developer tools.
  2. Before consent, interact with keyed controls and confirm there are no requests to /api/collect.
  3. Grant consent and trigger one deliberate event.
  4. In the Network panel, confirm a POST to the configured collector returns 202.
  5. 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.
  6. Revoke consent, trigger another interaction and confirm no request is sent.
  7. Confirm flux.behaviour.visitor_id and flux.behaviour.session_id are absent after revocation.
  8. Sign in to the Flux dashboard and confirm the accepted event appears in the recent journey narrative.
  9. 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
Confirm consent was granted, the event has a valid class, action, role and key, and both script attributes are present. Pre-consent and invalid events are intentionally dropped.
The collector returns 403
Compare the browser's exact 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
If you call the API directly, validate against schema version 1.2.0 and supply every required field. Prefer the SDK, which builds and validates the envelope.
Events appear more than once
Make sure the tag is installed once and SPA components do not register duplicate handlers. Do not combine automatic capture with a manual event for the same interaction unless both records are intentional.
Google sign-in is unavailable
The runtime returns 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
The tag is deliberately fire-and-forget and does not retry. First confirm the collector returned 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