Quick Start
EXOS Analytics is an operational intelligence platform built on ClickHouse Cloud, the HyperDX browser SDK, and Vercel serverless functions. It runs session replay, product analytics, process mining, and AI-powered querying over your own OpenTelemetry data — a supplement to product-analytics tools like Pendo or Amplitude, not a drop-in replacement for them.
Explore the live portal
Start with Process Mining to see operator patterns, then jump to Session Replay for full DOM recordings.
Ask a question in natural language
Open Ask AI and try: "Which operators handle the most orders?" or "Show me sessions with rage clicks"
Read the architecture docs
Dive into the documents below for system architecture, data model, storage tiering, and competitive analysis.
Process Mining
Operator Patterns leaderboard and Order Lifecycle funnel analysis
Sessions
Full DOM session replay with drill-down tabs and frustration signals
Product Analytics
Sessions, visitors, top pages, browser/OS breakdowns, user journeys
Site Health
Error rates, Web Vitals, long tasks, console errors, HTTP monitoring
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- | ------> | GCP us-central1 |
| | 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 | Serverless Fns | SQL | |
| (Vercel) | <-----> | (Vercel) | <-----> | Join key: |
| Vanilla HTML | REST | 24 endpoints | Query | rum.sessionId |
+------------------+ +-------------------+ +------------------------+
|
| /api/insights calls
v
+-------------------+
| Claude (Azure) |
| (Ask AI) |
+-------------------+
ClickHouse Cloud
Columnar OLAP on GCP us-central1. Sub-second analytical queries.
HyperDX SDK
OTEL-compatible browser SDK: DOM replay, traces, console, network.
OTEL Collector
Receives telemetry, batches inserts to ClickHouse.
API (Vercel Functions)
24 serverless endpoints for stats, sessions, insights, funnels, OCPM, and more.
Static Portal (Vercel)
Vanilla HTML + Tailwind CSS. No build step, no framework.
Ask AI (Claude)
Natural language to SQL. Conversation memory with follow-ups.
Concepts & Data Model
Three ideas explain everything in EXOS Analytics: the OpenTelemetry model (how events are shaped), properties (how data is segmented by product), and the core tables (where it lands in ClickHouse). The detailed column-level schema and example queries follow in Data Model below.
The OpenTelemetry model
EXOS uses the OpenTelemetry (OTel) data model end-to-end. Three nested concepts map directly onto ClickHouse columns:
A complete workflow or browsing session, identified by TraceId. All spans that share a TraceId belong to the same trace; spans also share a session via ResourceAttributes['rum.sessionId'].
One event — a click, an HTTP call, a page view, or a business event like order.closed. Each span is one row in otel_traces, named by SpanName, timed by Timestamp + Duration.
Key-value metadata stored as Map(String, String). SpanAttributes holds event-specific keys (operator.id, lob, order.id); ResourceAttributes holds session-level keys. Access with bracket syntax: SpanAttributes['operator.id'] — never as a bare column.
Properties — segmenting by ServiceName
Every span carries a ServiceName. EXOS groups services into properties — the distinct products and sites that report telemetry to the same store. Scope any view to a single property by passing ?workspace=<ServiceName>; /api/workspaces lists the properties currently reporting data.
| Property | ServiceName | What it is |
|---|---|---|
| EXOS Operations | exos-orders, exos-delivery, exos-inspection, exos-scheduling, exos-assignments, exos-internal-tools, exos-portal-ui | The EXOS order-operations platform (the primary product under analysis) |
| Analytics Portal | clickstack-portal | This analytics portal itself — it is instrumented with its own SDK (dogfooding) |
| RadLo | radlo-web | The RadLo web application |
| Docs | beppe-dotfiles-docs | A documentation site reporting RUM telemetry |
The default product-analytics views scope to EXOS + Portal — ServiceName LIKE 'exos-%' OR ServiceName = 'clickstack-portal' (defined once in functions/api/_shared.js as SERVICE_FILTER). Other properties are queryable explicitly via the workspace parameter.
Core tables
Telemetry lands in the default database (raw OTel, collector-owned); EXOS process-mining derivations live in the exos database. Row counts are from a live read of the backing ClickHouse instance on 2026-06-23.
default — OpenTelemetry + session replay
| Table | Rows | Role |
|---|---|---|
| otel_traces | ~792,000 | Spans: browser RUM + business events. The primary analytics table. |
| hyperdx_sessions | ~414,800 | rrweb DOM recordings — the session-replay payload (ScopeName = 'rum.rr-web'). |
| otel_logs | 20,648 | Captured browser console output and structured app logs. |
| otel_metrics_gauge / _histogram / _sum | ~622K | Web Vitals and application metrics. |
| logs, errors | views | Convenience views over the tables above. |
exos — process mining + platform reference
| Table | Rows | Role |
|---|---|---|
| ocpm_order_events | 29,485 | Object-centric event log driving the OCPM dashboard (see OCPM). |
| mortgage_telemetry | 2,079 | Domain telemetry for the mortgage workflow. |
| session_enrichment, operator_paths, object_interactions_agg | stub / empty | Derived-aggregate tables defined for process-mining views but currently unpopulated (operator_paths: 1 row, object_interactions_agg and mv_object_interactions: 0 rows; session_enrichment: 42 rows). Process-mining surfaces read live from otel_traces instead. |
| api_endpoints, service_catalog, work_order_fields, bundle_models, platform_metrics, wiki_pages | lookup | Small platform-reference / lookup tables. |
Only default and exos are reachable from any portal surface. See Access & Auth for how isolation is enforced.
Documentation
The live portal surfaces are the canonical reference. Detailed design documents (system architecture, data patterns, OCPM, storage tiering, competitive analysis) are maintained in the project repository and are intentionally not served on the public portal.
System Architecture
Live architecture page: data flow from the HyperDX SDK through the OTEL Collector to ClickHouse Cloud (GCP us-central1), with the API layer and static portal on Vercel.
ArchitectureObject-Centric Process Mining
ClickHouse-native OCPM using standard SQL — cross-object patterns, vendor contention, and category interaction graphs. See the OCPM section on this page.
Process MiningData 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
~415K events]
A -->|User events
clicks, HTTP, page loads, domain events| C[otel_traces
~792K 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
| Column | Type | Description |
|---|---|---|
| Timestamp | DateTime64 | Event time (nanosecond precision) |
| SpanName | String | Event type (see taxonomy below) |
| Duration | UInt64 | Span duration in nanoseconds |
| SpanAttributes | Map(String, String) | Event-specific metadata (http.url, visitor.*, operator.*, etc.) |
| ResourceAttributes | Map(String, String) | Session-level metadata (rum.sessionId, service.name, etc.) |
SpanName taxonomy
| Category | SpanName | Count | Meaning |
|---|---|---|---|
| User Interactions | |||
| click | 1,123 | Mouse click on any element | |
| mousedown | 305 | Mouse button pressed | |
| mouseup | 286 | Mouse button released | |
| Network | |||
| HTTP POST | 10,869 | Outbound HTTP POST (API calls) | |
| HTTP GET | 829 | Outbound HTTP GET (page/asset loads) | |
| resourceFetch | 1,412 | Resource timing (scripts, stylesheets, images) | |
| Performance | |||
| longtask | 4,668 | Main thread blocked >50ms | |
| webvitals | 283 | Core Web Vitals (LCP, FID, CLS, TTFB) | |
| documentLoad | 149 | Full page load timing | |
| Navigation | |||
| visibility | 262 | Tab visibility change (focus/blur) | |
| page.order_detail | 519 | Navigated to order detail view | |
| page.scheduling | 373 | Navigated to scheduling view | |
| page.inbox | 356 | Navigated to inbox | |
| page.documents | 347 | Navigated to documents view | |
| Domain Events | |||
| email.read | 767 | Operator opened an email | |
| email.received | 748 | Inbound email arrived | |
| email.sent | 711 | Operator sent an email | |
| review.started | 187 | Review process initiated | |
| report.submitted | 177 | Report submitted to client | |
| Replay | |||
| record init | 149 | rrweb 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
| Column | Type | Description |
|---|---|---|
| session_id | String | Session identifier (matches rum.sessionId) |
| events | String (JSON) | Serialized array of rrweb events |
| timestamp | DateTime64 | Batch 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
| Column | Type | Description |
|---|---|---|
| Timestamp | DateTime64 | When the log was emitted |
| SeverityText | String | info (13,199), debug (7,422), warn (27) |
| Body | String | Log message content |
| ResourceAttributes | Map(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().
| Attribute | Source | Example |
|---|---|---|
| visitor.city | geo.city | Chicago |
| visitor.region | geo.region | Illinois |
| visitor.country | geo.country | US |
| visitor.org | network.asOrganization | Comcast Cable |
| visitor.asn | network.asn | 7922 |
| visitor.browser | device.browser | Chrome |
| visitor.browser_version | device.browserVersion | 122.0.0.0 |
| visitor.os | device.os | macOS |
| visitor.os_version | device.osVersion | 14.3.1 |
| visitor.device_type | device.type | desktop |
| visitor.ip | visit.ip | 73.162.x.x |
| visitor.language | visit.acceptLanguage | en-US |
| visitor.returning | visit.isReturning | true |
| visitor.visit_count | visit.visitCount | 14 |
| visitor.referer | request.referer | https://google.com/ |
Session Identity
When a user signs in via Firebase Auth, their identity is written to all subsequent spans using HyperDX.setGlobalAttributes():
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.
| Tab | Data Source | SpanName Filter |
|---|---|---|
| Events | otel_traces | click, mousedown, mouseup, visibility, page.* |
| Network | otel_traces | HTTP POST, HTTP GET, resourceFetch |
| Console | otel_logs | All severity levels |
| Errors | otel_logs + otel_traces | SeverityText = 'error' + error-status traces |
| Perf | otel_traces | webvitals, longtask, documentLoad |
Frustration Signal Detection
dead-click-tracker.js detects four frustration signals via HyperDX.addAction():
| Signal | Action | Rule |
|---|---|---|
| Rage Click | rage_click | >4 clicks on same element within 2s |
| Dead Click | dead_click | No DOM mutation within 500ms of click |
| Excessive Scroll | excessive_scroll | >3 direction changes AND >130% doc height |
| Quick Back | quick_back | A->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 10Sessions 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 DESCConsole 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 10Process 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.
| Function | Purpose | Celonis Equivalent |
|---|---|---|
| sequenceMatch() | Happy path conformance | Process Conformance |
| sequenceCount() | Rework loop detection | Rework Analysis |
| lagInFrame() | Transition timing | Bottleneck Analysis |
| windowFunnel() | Funnel conversion | Conversion Funnel |
| groupArray() | Variant paths | Process Variants |
| quantile() | P95 bottlenecks | KPI Thresholds |
| rolling z-score | Anomaly detection | Anomaly 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 DESCTransition 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 10SDK & 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:
| Category | SpanName Prefix | Examples |
|---|---|---|
| Communication | email.* | email.read, email.sent, email.received |
| Portal | page.* | page.order_detail, page.scheduling, page.inbox |
| System Action | action.* | action.update_status, action.assign_vendor |
| Order Lifecycle | order.* | order.created, order.closed |
| Quality Control | review.* | review.started |
| Document | report.* | report.submitted |
| Field Work | inspection.* | inspection.scheduled |
| Valuation | appraisal.* | appraisal.assigned |
ClickHouse Primitives for OCPM
Five SQL queries power the OCPM dashboard, using eight ClickHouse primitives:
| Function | OCPM Purpose | Celonis Equivalent |
|---|---|---|
| arrayJoin() | Explode category arrays into co-occurrence pairs | Object Interaction Graph |
| arrayDistinct() | Deduplicate categories per order | N/A (PQL engine) |
| groupArray() | Reconstruct event sequences per category | Process Variant Analysis |
| cityHash64() | Fast variant hashing for counting | Proprietary indexing |
| topK() | Approximate top-K path patterns | Top Variants view |
| lagInFrame() | Cross-category transition timing | Bottleneck Analysis (OCPM) |
| quantile() | P50/P95 lifecycle durations | KPI Thresholds |
| dateDiff() | Object lifecycle span computation | Throughput 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
| Capability | Celonis OCPM | EXOS OCPM |
|---|---|---|
| Cost model | Commercial license | Runs on existing ClickHouse |
| Query language | PQL (proprietary) | Standard SQL |
| Setup time | Weeks | Zero |
| Schema changes | Required | None |
Comparison covers EXOS' SQL-native OCPM approach vs. a proprietary process-mining suite; it is not a like-for-like product replacement claim.
API & Query Reference
Every portal view is backed by a JSON endpoint under /api/*. All endpoints query ClickHouse in real time (no caching layer) and run as the least-privilege exos_readonly user. Responses are JSON with permissive CORS.
Conventions
Method: GET unless noted; /api/query and /api/sql are POST (JSON body).
Time window: hours — lookback in hours, clamped per-endpoint (commonly 1–720, default 168). Process-mining endpoints default to 720 (30 days).
Dimension filters (most analytics endpoints): workspace (a ServiceName / property), lob (line of business), operator (operator.id), task (core.task).
Errors: 400 for a missing/invalid required parameter, 500 with { "error": "…" } on a query failure.
Product analytics & metrics
| Endpoint | Purpose | Key parameters |
|---|---|---|
| GET /api/stats | KPI snapshot: sessions, events, error rate. | workspace, lob, operator, task, hours |
| GET /api/events | Raw trace/log feed with search + pagination. | table (otel_traces|otel_logs), hours, workspace, search, status, limit, offset, cursor, lob, operator, task |
| GET /api/funnels | Step conversion via windowFunnel(). | steps (CSV), hours, workspace, lob, operator, task, list |
| GET /api/retention | Cohort retention matrix. | first_action, return_action, granularity (daily|weekly), hours, workspace, lob, operator, task, list |
| GET /api/paths | User-journey / path discovery. | hours, depth, workspace, lob, operator, task |
| GET /api/errors | Grouped error analysis + timeline. | hours, workspace, group, lob, operator, task |
| GET /api/workspaces | Lists properties (services) with event + session counts. | — |
| GET /api/lobs | Lists lines of business with volumes. | — |
| GET /api/filters | Cascading filter options (LOBs, operators, tasks, mailboxes). | lob, operator |
Sessions & replay
| Endpoint | Purpose | Key parameters |
|---|---|---|
| GET /api/sessions | Session list; pass id for one enriched session's spans. | id (TraceId), hours, limit (1–200), lob, operator, task, span |
| GET /api/sessions-by-context | Sessions filtered by task / operator / object category. | task, operator, category, hours, limit |
| GET /api/visitor | Resolves the caller's IP into geo/device/network attributes (called on page load). | — |
Process mining
| Endpoint | Purpose | Key parameters |
|---|---|---|
| GET /api/operators | Operator leaderboard; pass id for a single-operator profile. | id, hours, lob, role |
| GET /api/insights | Narrative process-mining insights; self-documents the ClickHouse primitives used. | hours |
| GET /api/ocpm | Object-centric process mining (interactions, variants, lifecycle, actor-affinity). | hours (default 720, max 2160) |
| GET /api/patterns | Anomaly detection over span volume (rolling z-score). | hours, min_hours, threshold |
| GET /api/task-detail | Drill-down for one task / SpanName: sequences, entities, exit distribution. | task (required), hours |
AI & SQL
| Endpoint | Purpose | Body / parameters |
|---|---|---|
| POST /api/query | Natural-language → SQL (Claude on Azure), validated and executed read-only. Returns rows + the generated SQL. | { question } or { messages[] } |
| POST /api/sql | Execute a raw read-only SELECT/WITH. DDL/DML and cross-database refs are rejected. | { sql, database? (default|exos) } |
| GET /api/schema | Column catalog — covers only 4 named tables (default.otel_traces, default.sessions, exos.mv_object_interactions, exos.ocpm_order_events), not the full set of queryable tables. | — |
| POST /api/explain | AI plain-language summary of a result set. | { …, summarize? } |
Both AI/SQL paths enforce an allowlist of { default, exos } and reject any reference to system or personal databases — see Access & Auth.
Example: request & response
KPI snapshot, scoped to one property
GET /api/stats?workspace=exos-orders&hours=168
{
"sessions": <count>,
"events": <count>,
"error_rate": <0..1>,
"window_hours": 168,
"workspace": "exos-orders"
}
Illustrative shape only — field names and the exact response vary by endpoint, and the live values depend on the selected window and property. Use Ask AI to explore without writing SQL.
Run a read-only query directly
POST /api/sql
Content-Type: application/json
{ "sql": "SELECT ServiceName, count() AS spans FROM otel_traces WHERE Timestamp >= now() - INTERVAL 7 DAY GROUP BY ServiceName ORDER BY spans DESC" }
Access & Auth
The portal has two ways in — an authenticated sign-in for normal use, and a read-only demo mode for evaluation. Both reach exactly the same isolated demo dataset; neither can reach anything else on the cluster.
Sign-in (Firebase Auth)
Sign-in is handled by Firebase Authentication with two OAuth providers:
On a successful sign-in the user's identity (user.email, user.name, user.uid) is attached to subsequent telemetry via HyperDX.setGlobalAttributes(), and cached in localStorage (exos_auth_user) to avoid a login flash on navigation. Signing out clears the cache and restores the gate.
Demo access mode (?skipauth=true)
Appending ?skipauth=true to any URL (also skipAuth / skip_auth) bypasses the front-end sign-in overlay so reviewers and test automation can browse without a credential:
https://exos-demo.com/sessions?skipauth=true
Sets localStorage.exos_skip_auth = 'true' and persists across navigation. Clear it (localStorage.removeItem('exos_skip_auth')) to re-enable the sign-in gate.
Creates a labelled placeholder user for display only — it is not a real credential and grants no additional privilege.
What demo mode can — and can't — reach
Skipping the front-end gate does not widen data access. Data isolation is enforced server-side, in three independent layers, and applies identically to signed-in and skip-auth requests:
Least-privilege database user. Every query runs as exos_readonly, a ClickHouse user GRANTed SELECT on the default and exos databases only.
Engine-level read-only. That user runs with readonly=1, so table-function escapes (remote(), url(), s3(), merge()) are rejected by the engine, not just by application code.
App-layer allowlist. NL-to-SQL (/api/query) and raw SQL (/api/sql) independently allow only { default, exos } and reject cross-database references, DDL, and DML.
The result: the demo surfaces exactly the OpenTelemetry traces, session replay, logs, and process-mining tables described in Concepts & Data Model — and nothing else. Any other data co-resident on the instance is unreachable through every portal surface. Session replay additionally records with maskAllInputs: true, redacting form-field values before they leave the browser.
Keyboard Shortcuts
Press ? on any page. Press Esc to close.
Session Replay: Space (play/pause), Left/Right (seek), F (fullscreen).