Every query, every click, every AI rescue.
An append-only event table feeds seven dashboard tabs, a live activity feed, a latency histogram with real percentiles, and the AI Coach that turns the data into one-click fixes.
One row per search. No PII, ever.
Every query writes one row to search_events — an append-only
table. Skryx never stores IPs, user agents, or user IDs; the data is
aggregated at query time, not pre-hashed. Click signal arrives as a
nullable update on the same row.
// search_events row { "tenant_id": 42, "index_id": 7, "query": "wireless headphones", "results_count": 142, "clicked_doc_id": "sku-001", "clicked_position": 2, "search_mode": "hybrid", "ai_enhanced": true, "ai_interventions": { "typo_fix": false, "query_rewrite":false, "synonym_used": true }, "response_time_ms": 18, "embed_time_ms": 3, "dwell_time_ms": 8200, "quality_score": 0.87, "occurred_at": "2026-05-25T17:14:02Z" }
Twelve columns.
Designed for triage, not surveillance.
The shape is deliberately small: query text + result count + click position + mode + AI intervention flags + latency. Anything beyond that isn't worth storing. Click events are not a separate table — they arrive as a post-fact update on the original search row, so query and outcome stay correlated without a join.
- Append-only:
UPDATED_ATis null on the model — clicks update via explicit UPDATE, not Eloquent magic - Indexed: on (tenant, occurred_at), (index, results_count), (tenant, ai_enhanced, occurred_at), (tenant, search_mode)
- No IP, no UA, no user ID — aggregation happens at SQL
GROUP BYtime
Seven tabs. Each one tells a different story.
The four numbers your boss asks for.
Total searches, zero-result count, average latency, AI-enhanced count. Plus a volume timeseries chart, granularity auto-picked: hourly for ranges ≤ 24h, daily for longer. No pre-computed rollups — Skryx aggregates on demand, so the data is always current.
- Range presets: 24h / 7d / 30d / 90d
- JSON API endpoint:
/api/dashboard/timeseries
Real p50 / p75 / p95 / p99.
Histogram with 10ms buckets.
Latency percentiles computed from the actual response_time_ms
column at query time — no sampling, no approximation. Histogram covers
0–500 ms in 10 ms buckets plus a tail bucket for everything above 500 ms.
Slowest queries listed separately with their query text for inspection.
- p50, p75, p95, p99 + mean latency
- 50-bucket histogram (0–500 ms) +
tail_500ms_pluscount - Top 20 slowest queries with average latency and last-seen timestamp
Sortable, paginated, drillable.
Paginated table (25/page) of unique queries: volume, average results, average latency, CTR clicks, last seen. Sort by any column. Click into any query to see the result set Skryx returned during the period. Plus a day-of-week × hour heatmap (7 × 24) so you can spot traffic patterns at a glance.
- Sort: count / latency / zero-results / recent
- Heatmap: weekly traffic shape, 168 cells
- JSON:
/api/analytics/queries,/api/analytics/heatmap
Skryx AI's contribution, quantified.
A dedicated tab counts every AI intervention by family:
typo_fix, synonym_used, query_rewrite,
semantic_dispatched, hybrid_dispatched. The
headline metric — queries rescued from zero results —
counts every search where keyword would have returned nothing but the
AI pipeline produced hits.
- Per-intervention breakdown (5 families)
- AI-rescued zero-results count
- JSON:
/api/analytics/ai
The queries you're losing customers on.
Volume-sorted list of zero-result queries with AI-rescued count split out. This is the table AI Coach reads to suggest synonyms and catalog gaps — so every recommendation has a direct link back to the underlying queries.
- Zero-result rate (%) + count
- AI-rescued count (how many became non-zero via Skryx AI)
- One-click jump to "Add synonym" with the query pre-filled
Curated rules + their impressions / CTR.
Every Visual Curator rule (pin / replace / hide) records impressions, clicks, and which trigger fired. A separate tab in the analytics surface shows per-rule CTR plus last-fired timestamps so editorial decisions stay accountable to traffic data.
- Per-rule: impressions, clicks, CTR, last fired, active flag
- Built-in A/B comparison for rule variants
- Same data via the per-rule analytics API endpoint
Know the euro figure search earned you. Last week, this month, last year.
First-search-wins attribution per session — every order tied back to the FIRST search that started the buyer's journey. Funnel widget breaks down searches → cart adds → purchases. Revenue attributed in your display currency. Compare to previous period. Drill down to which queries earned the most.
What customers did after they searched.
ConversionAttributionService watches order.completed
events, walks back through the session events strictly before the
order, finds the FIRST search.performed, and credits that query
with the revenue. Cart adds get the same treatment via cart.add.
- Search-attributed revenue per period with vs-previous delta
- Conversion rate, AOV, revenue per search — at a glance
- Top queries by revenue, top products by search-driven sales
- Daily series chart, per-tenant currency conversion at write time
- Inflation-safe: an order with no preceding search is NOT attributed
GET /api/analytics/conversion?period=30d
{ "funnel": {
"searches": 583,707,
"cart_adds": 10,716,
"purchases": 3,491,
"search_to_purchase_pct": 0.60 },
"revenue": {
"search_attributed": 1062035.01,
"vs_previous_pct": +14.3,
"search_share_pct": 60.8,
"aov_attributed": 304.21 },
"top_queries": […],
"top_products": […] }
Per-stage latency. Streamed CSV. Weekly email digest.
Every search response now carries a _meta.timings block with
per-stage millis: ai_understanding, keyword,
vector, merge, rerank. Lets you see
exactly WHERE the time went on slow queries.
CSV exports stream chunked from every analytics endpoint — queries, conversions, zero-results — without OOM on large windows. UTF-8 BOM so Excel opens accents cleanly. Pro+ plans get a weekly digest mail with the Search → Purchase summary.
GET /api/analytics/export.csv?type=conversions&period=30d- Per-stage timing in every search response under
_meta.timings - Weekly Search → Purchase digest email · Pro+
- Plan-aware retention clamp — your window matches what you pay for
// Per-search timing breakdown { "_meta": { "timings": { "ai_understanding": 12, "keyword": 8, "vector": 24, "merge": 3, "rerank": 6 }, "corrected_query": "alternator", "used_synonyms": [ {"matched":"alternator", "expanded_to":["dinam","generator auto"]} ] } }
Two clever bits worth calling out.
Distinguishes typos from rewrites.
If the engine's final query differs from the original, Skryx checks
Levenshtein distance. ≤ 2 edits and not already marked
as an LLM rewrite → flagged as typo_fix with from/to
recorded. Anything farther afield is a query_rewrite.
That separation makes the AI Impact tab honest about what's helping.
{
"original_query": "sneekers",
"final_query": "sneakers",
"levenshtein": 1,
"ai_interventions": {
"typo_fix": {
"from": "sneekers",
"to": "sneakers"
}
}
}
Every chart is a queryable endpoint.
Five endpoints cover the dashboard: timeseries, latency histogram, queries, heatmap, AI breakdown. All return JSON, all accept a period parameter. Pipe them into Metabase, Looker, or a homegrown report — Skryx doesn't try to be your BI tool.
/api/dashboard/timeseries/api/analytics/latency-histogram/api/analytics/queries(paginated)/api/analytics/heatmap(7 × 24)/api/analytics/ai
GET /api/analytics/queries
?period=7d&page=1&sort=count
{ "rows": [
{ "q": "sneakers",
"vol": 1420,
"hits": 142,
"ctr": 0.42,
"avg_ms": 17,
"last_seen": "2026-05-25T17:12Z" },
…
],
"page": 1,
"per_page": 25 }
EU-hosted. No PII. No third-party trackers.
Storage in Frankfurt. No IP, user-agent, or user-id captured. Click data
arrives as a nullable update on the existing row, not a separate event
broadcast. Aggregation happens at SQL GROUP BY time, so
individual searches never appear in dashboard counters.
/api/analytics/* for every dashboard view