ServiceLink EXOS | Analytics

Quick Start

EXOS Analytics is an operational intelligence platform built on ClickHouse Cloud, HyperDX, and AKS. It unifies session replay, product analytics, process mining, and AI-powered querying into a single platform -- replacing tools that would cost $200K+/yr from Amplitude, FullStory, Celonis, and others.

1

Explore the live portal

Start with Process Mining to see operator patterns, then jump to Session Replay for full DOM recordings.

2

Ask a question in natural language

Open Ask AI and try: "Which operators handle the most orders?" or "Show me sessions with rage clicks"

3

Read the architecture docs

Dive into the documents below for system architecture, data model, storage tiering, and competitive analysis.

Architecture Overview

All browser telemetry flows through a single pipeline: HyperDX SDK captures DOM snapshots, traces, and console logs, sends them through an OpenTelemetry Collector, and lands them in three ClickHouse tables. The portal and API layer read from those tables to power every view.

                         EXOS Analytics -- System Architecture

  +------------------+         +-------------------+         +------------------------+
  |  Browser Client  |  OTEL   |  OTEL Collector   |  Batch  |   ClickHouse Cloud     |
  |  (HyperDX SDK)   | ------> | collector.exos-   | ------> |   Azure East US 2      |
  |                  |  HTTP   |  demo.com         |  Insert |                        |
  +------------------+         +-------------------+         |  +------------------+  |
        |                                                     |  | otel_traces      |  |
        | Captures:                                           |  | (clicks, HTTP,   |  |
        |  - DOM snapshots (rrweb)                            |  |  page loads,     |  |
        |  - User interactions                                |  |  domain events)  |  |
        |  - HTTP requests                                    |  +------------------+  |
        |  - Console output                                   |  | hyperdx_sessions |  |
        |  - Web Vitals                                       |  | (rrweb DOM       |  |
        |  - Domain events                                    |  |  recordings)     |  |
                                                              |  +------------------+  |
                                                              |  | otel_logs        |  |
                                                              |  | (console output) |  |
  +------------------+         +-------------------+          |  +------------------+  |
  |  Static Portal   |  API    |  Node.js API      |  SQL    |                        |
  |  (Vercel)        | <-----> | (AKS)             | <-----> |  Join key:             |
  |  Vanilla HTML    |  REST   | 21 endpoints      |  Query  |  rum.sessionId         |
  +------------------+         +-------------------+         +------------------------+
                                      |
                                      | /api/insights calls
                                      v
                               +-------------------+
                               |  OpenAI GPT-4.1   |
                               |  (Ask AI)         |
                               +-------------------+

ClickHouse Cloud

Columnar OLAP on Azure East US 2. Sub-second queries at $170/mo.

HyperDX SDK

OTEL-compatible browser SDK: DOM replay, traces, console, network.

OTEL Collector

Receives telemetry, batches inserts to ClickHouse.

Node.js API (AKS)

21 endpoints for stats, sessions, insights, funnels, OCPM, and more.

Static Portal (Vercel)

Vanilla HTML + Tailwind CSS. No build step, no framework.

Ask AI (GPT-4.1)

Natural language to SQL. Conversation memory with follow-ups.

Documentation

Architecture documents covering system design, data patterns, competitive positioning, and operational intelligence techniques.

Data Architecture

All browser telemetry is captured by the HyperDX Browser SDK and sent to the OTEL Collector at collector.exos-demo.com, which writes to three ClickHouse tables. Every table shares a common join key: ResourceAttributes['rum.sessionId'].

Data Flow

graph TD
    A[Browser SDK HyperDX] -->|DOM snapshots
rrweb format, video replay| B[hyperdx_sessions
~33K events] A -->|User events
clicks, HTTP, page loads, domain events| C[otel_traces
~18K spans] A -->|Console output
log, warn, error, debug| D[otel_logs
~21K logs] classDef default fill:#1e1e2e,stroke:#333,color:#d4d4d4; classDef edgeLabel fill:#1e1e2e,color:#d4d4d4,stroke:none;

All joined by: ResourceAttributes['rum.sessionId']

otel_traces

Every user interaction, HTTP request, page load, performance event, and domain event. This is the primary analytics table.

Key columns

ColumnTypeDescription
TimestampDateTime64Event time (nanosecond precision)
SpanNameStringEvent type (see taxonomy below)
DurationUInt64Span duration in nanoseconds
SpanAttributesMap(String, String)Event-specific metadata (http.url, visitor.*, operator.*, etc.)
ResourceAttributesMap(String, String)Session-level metadata (rum.sessionId, service.name, etc.)

SpanName taxonomy

CategorySpanNameCountMeaning
User Interactions
click1,123Mouse click on any element
mousedown305Mouse button pressed
mouseup286Mouse button released
Network
HTTP POST10,869Outbound HTTP POST (API calls)
HTTP GET829Outbound HTTP GET (page/asset loads)
resourceFetch1,412Resource timing (scripts, stylesheets, images)
Performance
longtask4,668Main thread blocked >50ms
webvitals283Core Web Vitals (LCP, FID, CLS, TTFB)
documentLoad149Full page load timing
Navigation
visibility262Tab visibility change (focus/blur)
page.order_detail519Navigated to order detail view
page.scheduling373Navigated to scheduling view
page.inbox356Navigated to inbox
page.documents347Navigated to documents view
Domain Events
email.read767Operator opened an email
email.received748Inbound email arrived
email.sent711Operator sent an email
review.started187Review process initiated
report.submitted177Report submitted to client
Replay
record init149rrweb recording session started

Example query

SELECT SpanName, count() as cnt FROM otel_traces WHERE Timestamp >= now() - INTERVAL 7 DAY GROUP BY SpanName ORDER BY cnt DESC LIMIT 20

Used by: Operator Patterns, Order Lifecycle, Product Analytics, Session Replay drill-down tabs

hyperdx_sessions

Full DOM recording data captured by rrweb. Each row contains a batch of rrweb events for a session, enabling pixel-perfect video replay.

Key columns

ColumnTypeDescription
session_idStringSession identifier (matches rum.sessionId)
eventsString (JSON)Serialized array of rrweb events
timestampDateTime64Batch recording time

Example query

SELECT session_id, count() as batches, min(timestamp) as started FROM hyperdx_sessions WHERE timestamp >= now() - INTERVAL 7 DAY GROUP BY session_id ORDER BY started DESC LIMIT 10

Used by: Session Replay player, activity heatbar

otel_logs

Console output captured from the browser. Every console.log(), console.warn(), console.error(), and console.debug() call is recorded.

Key columns

ColumnTypeDescription
TimestampDateTime64When the log was emitted
SeverityTextStringinfo (13,199), debug (7,422), warn (27)
BodyStringLog message content
ResourceAttributesMap(String, String)Session-level metadata (rum.sessionId, etc.)

Example query

SELECT SeverityText, count() as cnt FROM otel_logs WHERE Timestamp >= now() - INTERVAL 7 DAY GROUP BY SeverityText ORDER BY cnt DESC

Used by: Session Replay Console tab, Site Health error groups

Visitor Enrichment

On every page load, the portal calls /api/visitor to resolve the user's IP into geo, device, and network attributes via HyperDX.setGlobalAttributes().

AttributeSourceExample
visitor.citygeo.cityChicago
visitor.regiongeo.regionIllinois
visitor.countrygeo.countryUS
visitor.orgnetwork.asOrganizationComcast Cable
visitor.asnnetwork.asn7922
visitor.browserdevice.browserChrome
visitor.browser_versiondevice.browserVersion122.0.0.0
visitor.osdevice.osmacOS
visitor.os_versiondevice.osVersion14.3.1
visitor.device_typedevice.typedesktop
visitor.ipvisit.ip73.162.x.x
visitor.languagevisit.acceptLanguageen-US
visitor.returningvisit.isReturningtrue
visitor.visit_countvisit.visitCount14
visitor.refererrequest.refererhttps://google.com/

Session Identity

When a user signs in via Firebase Auth, their identity is written to all subsequent spans using HyperDX.setGlobalAttributes():

user.emailuser.nameuser.uid

Session Replay Architecture

Session replay reconstructs a pixel-perfect video of what the user saw and did. It combines rrweb DOM snapshots from hyperdx_sessions with behavioral telemetry from otel_traces and otel_logs.

How rrweb Records

The HyperDX SDK uses rrweb (v2.0.0-alpha.4) to capture the DOM. Three event types matter:

type 4 (Meta) -- Records viewport dimensions and page URL. Emitted at session start and on each navigation.

type 2 (Full Snapshot) -- Complete DOM serialization. The "keyframe" the replayer needs to build the page.

type 3 (Incremental Snapshot) -- Diffs: DOM mutations, mouse movements, scroll positions, input changes, viewport resizes.

How the Replayer Works

The rrweb Replayer reconstructs the page inside a sandboxed <iframe>. It deserializes the full snapshot (type 2) to build the initial DOM, then applies incremental events (type 3) in timestamp order. Compiled Tailwind CSS is injected so the replay retains original styling.

Seek-to-Time Mapping

When you click a row in any drill-down tab, the player jumps to that moment:

offset_ms = otel_traces.Timestamp - rrweb_first_event.timestamp

The absolute timestamp from otel_traces is converted to a millisecond offset from the first rrweb event. Implemented in seekToAbsTime().

Activity Heatbar

48px SVG waveform below the replay video showing activity density.

Bucketing: Up to 400 time buckets (min 50, 2px per bucket).

Weighted signals: click: 1.0, mousedown: 0.8, HTTP POST: 0.6, HTTP GET: 0.5, resourceFetch: 0.4, longtask: 1.5, documentLoad: 2.0. Scroll: 0.2.

Rendering: Catmull-Rom spline SVG. Errors = red circles, rage clicks = amber triangles, navigations = blue diamonds.

Interaction: Click to seek. White playhead tracks current position.

Drill-Down Tabs

Five tabs, each querying a different slice. Every row has a clickable timestamp that seeks the replay.

TabData SourceSpanName Filter
Eventsotel_tracesclick, mousedown, mouseup, visibility, page.*
Networkotel_tracesHTTP POST, HTTP GET, resourceFetch
Consoleotel_logsAll severity levels
Errorsotel_logs + otel_tracesSeverityText = 'error' + error-status traces
Perfotel_traceswebvitals, longtask, documentLoad

Frustration Signal Detection

dead-click-tracker.js detects four frustration signals via HyperDX.addAction():

SignalActionRule
Rage Clickrage_click>4 clicks on same element within 2s
Dead Clickdead_clickNo DOM mutation within 500ms of click
Excessive Scrollexcessive_scroll>3 direction changes AND >130% doc height
Quick Backquick_backA->B->A within 15 seconds

Query Cookbook

Use Ask AI for natural language queries or write SQL directly.

Natural Language Examples

Ask

"Which operators handle the most orders?"

Follow up

"Now filter to closing LOB"

Ask

"Show me sessions with rage clicks from returning visitors"

Ask

"What are the slowest API calls this week?"

SQL Examples

Sessions with rage clicks

SELECT ResourceAttributes['rum.sessionId'] as session_id, count() as rage_count
FROM otel_traces WHERE SpanName = 'rage_click'
GROUP BY session_id ORDER BY rage_count DESC

Slowest HTTP requests

SELECT SpanAttributes['http.url'] as url, avg(Duration) / 1e6 as avg_ms, count() as calls
FROM otel_traces WHERE SpanName IN ('HTTP POST', 'HTTP GET')
GROUP BY url ORDER BY avg_ms DESC LIMIT 10

Sessions from returning visitors in Chicago

SELECT DISTINCT ResourceAttributes['rum.sessionId'] as session_id
FROM otel_traces
WHERE SpanAttributes['visitor.city'] = 'Chicago'
  AND SpanAttributes['visitor.returning'] = 'true'

Web Vitals by page

SELECT SpanAttributes['location.href'] as page,
       avg(toFloat64(SpanAttributes['lcp'])) as avg_lcp_ms,
       avg(toFloat64(SpanAttributes['fid'])) as avg_fid_ms
FROM otel_traces WHERE SpanName = 'webvitals'
GROUP BY page ORDER BY avg_lcp_ms DESC

Console errors by session

SELECT ResourceAttributes['rum.sessionId'] as session_id, count() as error_count,
       groupArray(10)(Body) as sample_messages
FROM otel_logs WHERE SeverityText = 'error'
GROUP BY session_id ORDER BY error_count DESC LIMIT 10
Open Ask AI

Process Mining

EXOS performs legitimate process mining using ClickHouse's native sequence and window functions. No interviews, no manual process mapping, no proprietary query language.

Operator Patterns

The Operator Patterns page ranks operators by activity volume, task distribution, and automation opportunity.

Order Lifecycle

Three analysis modes:

Sequences -- Top 10 operator paths via groupArray() with AI-generated narrative insights.

Bottlenecks -- Transition timing via lagInFrame() and P95 via quantile(0.95).

Funnels -- Conversion analysis via windowFunnel().

ClickHouse Primitives

Seven functions power the analysis. The /api/insights endpoint self-documents every primitive.

FunctionPurposeCelonis Equivalent
sequenceMatch()Happy path conformanceProcess Conformance
sequenceCount()Rework loop detectionRework Analysis
lagInFrame()Transition timingBottleneck Analysis
windowFunnel()Funnel conversionConversion Funnel
groupArray()Variant pathsProcess Variants
quantile()P95 bottlenecksKPI Thresholds
rolling z-scoreAnomaly detectionAnomaly Alerts

Process Mining SQL Examples

Conformance check

SELECT countIf(matched) as conforming, count() as total,
       round(conforming / total * 100, 1) as conformance_pct
FROM (
  SELECT SpanAttributes['order.id'] as order_id,
    sequenceMatch('(?1)(?2)(?3)')(toDateTime(Timestamp),
      SpanName = 'email.received', SpanName = 'review.started',
      SpanName = 'report.submitted') as matched
  FROM otel_traces WHERE SpanAttributes['order.id'] != '' GROUP BY order_id
)

Rework detection

SELECT SpanAttributes['order.id'] as order_id,
  sequenceCount('(?1)(?2)')(toDateTime(Timestamp),
    SpanName = 'review.started', SpanName = 'revision.requested') as rework_loops
FROM otel_traces WHERE SpanAttributes['order.id'] != ''
GROUP BY order_id HAVING rework_loops > 0 ORDER BY rework_loops DESC

Transition bottlenecks

SELECT step_from, step_to, round(avg(gap_hours), 1) as avg_hours,
       round(quantile(0.95)(gap_hours), 1) as p95_hours
FROM (
  SELECT SpanName as step_to,
    lagInFrame(SpanName) OVER (PARTITION BY SpanAttributes['order.id'] ORDER BY Timestamp) as step_from,
    dateDiff('hour', lagInFrame(Timestamp) OVER (PARTITION BY SpanAttributes['order.id'] ORDER BY Timestamp), Timestamp) as gap_hours
  FROM otel_traces WHERE SpanAttributes['order.id'] != ''
) WHERE step_from != '' GROUP BY step_from, step_to ORDER BY p95_hours DESC LIMIT 10

SDK & Instrumentation

HyperDX Browser SDK

Every page includes the HyperDX SDK for DOM replay, console, network, and interaction traces.

<script src="https://www.unpkg.com/@hyperdx/browser@0.22.0/build/index.js"></script>
<script>
  window.HyperDX && window.HyperDX.init({
    apiKey: 'H06oX4xamF3QHsFN0hJn', service: 'clickstack-portal',
    url: 'https://collector.exos-demo.com',
    consoleCapture: true, advancedNetworkCapture: true,
    disableReplay: false, maskAllText: false, maskAllInputs: true,
  });
</script>

consoleCapture: true -- all console output to otel_logs

advancedNetworkCapture: true -- records headers/bodies for HTTP spans

disableReplay: false -- enables rrweb DOM snapshots

maskAllInputs: true -- redacts form field values for privacy

Visitor Enrichment

On page load, fetches visitor metadata and attaches to all spans:

fetch('/api/visitor').then(r => r.json()).then(v => {
  window.HyperDX && window.HyperDX.setGlobalAttributes({
    'visitor.country': v.geo?.country || '',
    'visitor.city':    v.geo?.city || '',
    'visitor.org':     v.network?.asOrganization || '',
    'visitor.browser': v.device?.browser || '',
    // ... (15 attributes total, see Data Architecture)
  });
});

Firebase Auth Identity Flow

Auth module attaches identity to all subsequent telemetry:

// auth.js onAuthStateChanged:
if (window.HyperDX) {
  window.HyperDX.setGlobalAttributes({
    'user.email': userData.email,
    'user.name':  userData.displayName,
    'user.uid':   userData.uid,
  });
}

Custom Domain Events

Logged via HyperDX.addAction(), appearing as spans in otel_traces:

HyperDX.addAction('rage_click', { element: 'button#submit', count: 6 });
HyperDX.addAction('email.sent', { order_id: '12345', template: 'scheduling_confirmation' });

Auth Bypass for Testing

For Playwright, Cypress, or manual testing:

https://exos-demo.com?skipAuth=true

Sets localStorage.exos_skip_auth = 'true', persists across loads. Clear to re-enable auth.

Object-Centric Process Mining

OCPM extends traditional case-centric analysis by recognizing that events can belong to multiple object types simultaneously. EXOS implements the first ClickHouse-native OCPM using standard SQL -- no proprietary query language required.

Why OCPM?

Case-centric mining sees one order at a time. OCPM reveals cross-object patterns:

Vendor contention -- when multiple orders compete for the same vendor, creating invisible bottlenecks

Document dependencies -- when a shared report blocks multiple downstream orders

Cross-category handoff delays -- the actual time lost when work transitions between Communication, Portal, and System Actions

Object Categories

Eight object categories derived from SpanName prefixes -- no schema changes needed:

CategorySpanName PrefixExamples
Communicationemail.*email.read, email.sent, email.received
Portalpage.*page.order_detail, page.scheduling, page.inbox
System Actionaction.*action.update_status, action.assign_vendor
Order Lifecycleorder.*order.created, order.closed
Quality Controlreview.*review.started
Documentreport.*report.submitted
Field Workinspection.*inspection.scheduled
Valuationappraisal.*appraisal.assigned

ClickHouse Primitives for OCPM

Five SQL queries power the OCPM dashboard, using eight ClickHouse primitives:

FunctionOCPM PurposeCelonis Equivalent
arrayJoin()Explode category arrays into co-occurrence pairsObject Interaction Graph
arrayDistinct()Deduplicate categories per orderN/A (PQL engine)
groupArray()Reconstruct event sequences per categoryProcess Variant Analysis
cityHash64()Fast variant hashing for countingProprietary indexing
topK()Approximate top-K path patternsTop Variants view
lagInFrame()Cross-category transition timingBottleneck Analysis (OCPM)
quantile()P50/P95 lifecycle durationsKPI Thresholds
dateDiff()Object lifecycle span computationThroughput Time

Example: Object Interaction Query

SELECT c1 AS category_a, c2 AS category_b,
  count() AS co_occurrence_count
FROM (
  SELECT oid,
    arrayJoin(categories) AS c1,
    arrayJoin(categories) AS c2
  FROM (
    SELECT SpanAttributes['order.id'] AS oid,
      arrayDistinct(groupArray(
        CASE WHEN SpanName LIKE 'email.%' THEN 'Communication'
             WHEN SpanName LIKE 'page.%' THEN 'Portal'
             WHEN SpanName LIKE 'action.%' THEN 'System Action'
        END
      )) AS categories
    FROM otel_traces
    WHERE SpanAttributes['order.id'] != ''
    GROUP BY oid
    HAVING length(categories) >= 2
  )
) WHERE c1 < c2
GROUP BY c1, c2
ORDER BY co_occurrence_count DESC

Uses arrayJoin() to create a cross-product of categories within each order, then counts co-occurrences.

Cost Comparison

CapabilityCelonis OCPMEXOS OCPM
Annual cost$100K+$0 incremental
Query languagePQL (proprietary)Standard SQL
Setup timeWeeksZero
Schema changesRequiredNone

Full documentation: docs/OCPM.md

Keyboard Shortcuts

Press ? on any page. Press Esc to close.

Show keyboard shortcuts?
Close overlayEsc

Session Replay: Space (play/pause), Left/Right (seek), F (fullscreen).