The two-minute version

Load the SDK, call init with your tenant and site. That gets you page views, click tracking on links and buttons, session stitching, device and OS classification, language, and country resolved server-side by GeoIP.

Everything after that is opt-in. The rest of this page is the configuration reference and the patterns that come up in practice — single-page apps, consent, identifying users, and mapping your own dimensions onto existing markup.

From signing in to your first report

The portal side of the integration, once through, in the order you actually do it.

  1. Sign in to the console

    Go to the console and sign in. Password sign-in works out of the box; if your organisation has SSO configured for its domain, Google, Microsoft and generic OIDC providers appear as buttons on the sign-in page and the provider list is resolved from the domain you are visiting.

    Nothing is billed and no card is taken. The account exists to hold your configuration — the data itself never lands in it.

    SSO
  2. Point the platform at your ClickHouse

    Open Infralytiqs → Settings and add a ClickHouse module: host, port, credentials and the database name. The platform provisions its own schema in that database, including the website_events table everything else is built on.

    If you do not have a ClickHouse yet, a single container is enough to get through this page — the Bring Your Own ClickHouse guide covers both the throwaway and the production shapes.

    Settings
  3. Record the tenant and site ids

    The module configuration carries a tenantId, a siteId and a dbName. Those two ids are the entire ingest contract: they appear in the collector URL as /il/analytics/{tenantId}/{siteId}/events, and the collector accepts an event only if the pair matches a saved configuration.

    Leaving siteId blank makes the configuration tenant-wide, which resolves to the __all_sites__ scope. Use a real site id unless you specifically want everything pooled.

    Settings
  4. Add the snippet

    Drop the two script tags below into your page template with those ids. That is the whole integration — page views, clicks, sessions, device and language start flowing immediately, and country is resolved server-side from the request by GeoIP.

    On a single-page app, take control of the page view instead: see the framework section further down.

    Your site
  5. Watch the first events land

    Load a page with ?ilDebug=1 appended and the SDK logs every event it queues and every batch it posts. In the network tab you are looking for a POST to /il/analytics/…/events returning 201.

    A 403 means the tenant and site pair does not match a saved configuration — almost always a typo in one of the two ids rather than anything deeper.

    Verify
  6. Build a report

    Open Infralytiqs → Reports, create a suite and add a panel. Pick dimensions, an aggregation and one of the visualisation types, and the builder writes the query. Any custom dimension you have started sending appears in the field picker within a few minutes.

    Reports

Getting a page view

Put this before the closing body tag. It is deliberately the whole thing.

<script src="https://assets.infralytiqs.com/v1/scripts/infralytiqs.min.js"></script>
<script>
  Infralytiqs.init({
    serverUrl: 'https://api.infralytiqs.com',
    tenantId:  'acme',
    siteId:    'acme-www'
  });
</script>

<!--
  That is enough for:
    · a page view on load, and on history changes
    · clicks on 'a, button, [data-il-track]'
    · a page-leave event
    · anonymous_id and session_id in first-party storage
    · device_type, device_os, language_iso_code
    · country_code, resolved server-side from the request IP by GeoIP
-->

Configuration reference

Every option on InfralytiqsConfig. Only the first three are required.

Option Type Default What it does
serverUrl string Required. Collector base URL
tenantId string Required. From Settings → Tenants
siteId string Required. From your ClickHouse module configuration
dbName string Optional. The server resolves the database authoritatively from the tenant and site, so this is only for debugging
evarMap Record<string, string \ (el) => string> CSS selectors or extractor functions that populate custom_dimensions on auto-captured clicks
propMap Record<string, (el) => number> The same idea for numeric custom_metrics
globalDimensions Record<string, string> Dimensions attached to every event. Ideal for release version or tenant
userId string Equivalent to calling identify() immediately
batchSize number 20 Events per POST. The server's hard limit is 1000
flushIntervalMs number 5000 How often a partial batch is sent anyway
sessionTimeoutMs number 1800000 Inactivity before a new session_id is minted — 30 minutes
clickSelector string 'a, button, [data-il-track]' Which elements auto-capture clicks
disableAutoPageView boolean false Turn off automatic page views when you want to send them yourself
disableAutoClick boolean false Turn off automatic click capture
disableAutoPageLeave boolean false Turn off the page-leave event
captureLocation boolean false Opt-in. Triggers the browser permission prompt and, on grant, adds precise coordinates to every event
debug boolean false Console logging of queued events and posted batches

Single-page applications

The SDK listens for history changes, so a page view fires on client-side navigation without any framework-specific code. What it cannot know is when your route has finished rendering, or what your route's logical name is — a URL like /orders/48210 is one route, not one page.

The pattern that works is to disable automatic page views and send them from your router, with a stable route name as a dimension. Then page_path stays accurate for drill-down while route gives you a groupable, low-cardinality dimension.

Framework integration

The same idea in four places: take control of the page view, and give the route a name you can group by.

import { useEffect } from 'react';
import { useLocation, useMatches } from 'react-router-dom';

// Init once, at app bootstrap, with auto page views off.
Infralytiqs.init({
  serverUrl: import.meta.env.VITE_IL_SERVER_URL,
  tenantId:  import.meta.env.VITE_IL_TENANT_ID,
  siteId:    import.meta.env.VITE_IL_SITE_ID,
  disableAutoPageView: true,
  globalDimensions: {
    app_version: import.meta.env.VITE_APP_VERSION ?? 'dev',
    render_mode: 'spa'
  }
});

export function useInfralytiqsPageViews() {
  const location = useLocation();
  const matches  = useMatches();

  useEffect(() => {
    // The route pattern, not the resolved URL — '/orders/:id' rather than
    // '/orders/48210'. This is the dimension you actually want to group by.
    const pattern = matches.length ? matches[matches.length - 1].id : location.pathname;

    Infralytiqs.track('page_view', {
      route:      pattern,
      page_url:   window.location.href,
      page_title: document.title,
      referrer:   document.referrer
    });
  }, [location.key]);
}

Consent, and mapping your existing markup

Two things every real deployment needs.

// The honest approach: do not initialise until you have a basis to.
// The SDK writes first-party storage for anonymous_id and session_id on
// init, so gating init is what actually prevents that.

function startAnalytics() {
  Infralytiqs.init({
    serverUrl: 'https://api.infralytiqs.com',
    tenantId:  'acme',
    siteId:    'acme-www'
  });
}

if (consentManager.has('analytics')) {
  startAnalytics();
} else {
  consentManager.on('granted', function (categories) {
    if (categories.includes('analytics')) { startAnalytics(); }
  });
}

// Withdrawal: destroy() removes listeners and stops the flush timer.
consentManager.on('withdrawn', function () {
  Infralytiqs.destroy();
});

// Worth knowing when you write your privacy notice: the collector resolves
// country_code from the request IP by GeoIP and does not store the IP, and
// precise coordinates require captureLocation plus a browser permission
// grant. Neither happens by default.

Common questions

It uses first-party browser storage for anonymous_id and session_id — same origin, never a third-party cookie, so nothing here depends on third-party cookies surviving. That is also why gating init behind consent is the effective control: no init, no identifiers written.
A session_id is minted on first activity and reused until 30 minutes of inactivity pass, configurable via sessionTimeoutMs. It is a client-side notion, so if you need a different definition — say, a session that never spans midnight — compute it in SQL from event_time instead. The raw events support any definition you like.
Queued events stay in memory and are retried on the next flush. They do not survive the page being closed, so a long outage does lose data. If that is unacceptable for a particular event, send it from your server instead, where you control durability.
Some blockers use filter lists that match analytics script names and hostnames, so yes, sometimes. Serving the SDK from your own domain and proxying the collector behind your own path defeats most of that — and since you control the collector URL, that is purely a configuration choice.
Yes. The ingest endpoint is plain JSON over HTTPS with no Authorization header, so fetch or navigator.sendBeacon works directly. You then own anonymous_id, session_id and device classification yourself, which is exactly what the mobile guide does.
The SDK classifies obvious bots as device_type = 'bot', so filter that value out in your reports. Filtering at report time rather than at ingest is the better default: the rows stay available, so you can always check what you excluded.

Other integrations

Mobile apps and server-side services use the same endpoint with no SDK at all.