feat(analytics): enrich query_logs metadata + query_log_insights view
For continuous optimization we need to attribute slow/failed decodes to the right cause. query_logs.timings jsonb is now a structured decode-meta blob, not just stage timings: - wmi: first 3 chars of VIN (per-brand aggregation) - result_kind: vehicle / pcat_candidates / emex_candidates / unknown / aborted - cache_source: db_hit / redis_positive / redis_negative / lock_wait / miss - candidate_pick: pcat / emex / none (when user picks from candidate modal) - pcat_car_count, emex_candidate_count (cardinality, drives candidate-modal rate) - pl24_circuit_open, pl24_skipped (CB state at request time) - vin_api_used, vin_api timing (NHTSA fallback frequency) Migration 0003 adds a query_log_insights VIEW that flattens these keys into typed columns, so ad-hoc SQL doesn't need json operators. New meta keys appear automatically as NULL; the VIEW stays stable. docs/analytics-queries.sql has 8 starter queries: cache hit ratio, per-source latency, slowest WMIs, stage breakdowns, CB/abort frequency, candidate-modal rate, top failing VINs, dedup effectiveness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
32
apps/api/drizzle/0003_query_log_insights.sql
Normal file
32
apps/api/drizzle/0003_query_log_insights.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- Analytics-friendly view over query_logs.
|
||||
-- Flattens commonly-queried keys from the `timings` jsonb so ad-hoc SQL
|
||||
-- doesn't need to remember every json path. New keys can be added without
|
||||
-- breaking existing queries (json operator returns NULL for missing keys).
|
||||
CREATE OR REPLACE VIEW "query_log_insights" AS
|
||||
SELECT
|
||||
ql.id,
|
||||
ql.user_id,
|
||||
ql.vin,
|
||||
substring(ql.vin from 1 for 3) AS wmi,
|
||||
ql.brand_id,
|
||||
ql.source,
|
||||
ql.success,
|
||||
ql.response_time_ms,
|
||||
ql.error_message,
|
||||
ql.created_at,
|
||||
(ql.timings->>'result_kind') AS result_kind,
|
||||
(ql.timings->>'cache_source') AS cache_source,
|
||||
(ql.timings->>'candidate_pick') AS candidate_pick,
|
||||
(ql.timings->>'pcat')::int AS pcat_ms,
|
||||
(ql.timings->>'emex')::int AS emex_ms,
|
||||
(ql.timings->>'pl24')::int AS pl24_ms,
|
||||
(ql.timings->>'vin_api')::int AS vin_api_ms,
|
||||
(ql.timings->>'lock_wait')::int AS lock_wait_ms,
|
||||
(ql.timings->>'pcat_car_count')::int AS pcat_car_count,
|
||||
(ql.timings->>'emex_candidate_count')::int AS emex_candidate_count,
|
||||
(ql.timings->>'pl24_circuit_open')::bool AS pl24_circuit_open,
|
||||
(ql.timings ? 'pl24_skipped') AS pl24_skipped,
|
||||
(ql.timings ? 'aborted') AS aborted,
|
||||
(ql.timings->>'vin_api_used')::bool AS vin_api_used,
|
||||
ql.timings AS raw_meta
|
||||
FROM query_logs ql;
|
||||
5173
apps/api/drizzle/meta/0003_snapshot.json
Normal file
5173
apps/api/drizzle/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,13 @@
|
||||
"when": 1778584574615,
|
||||
"tag": "0002_boring_the_stranger",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1778587000000,
|
||||
"tag": "0003_query_log_insights",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user