feat(server): add product analytics dashboard row
This commit is contained in:
@@ -60,6 +60,14 @@ OTel SDK 在导出到 Prometheus 时做两件事:
|
|||||||
| `ws.messages.sent` | Counter | 同上 | — |
|
| `ws.messages.sent` | Counter | 同上 | — |
|
||||||
| `ws.messages.received` | Counter | [services/domain/chats.ts](../../src/services/domain/chats.ts) | — |
|
| `ws.messages.received` | Counter | [services/domain/chats.ts](../../src/services/domain/chats.ts) | — |
|
||||||
|
|
||||||
|
## Product Analytics
|
||||||
|
|
||||||
|
| Metric | 类型 | 落点 | Labels |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `airi.product.events` | Counter | [services/domain/product-events.ts](../../src/services/domain/product-events.ts) `track` | `feature`、`action`、`status`、`source`(可选) |
|
||||||
|
|
||||||
|
> **Prometheus 只看事件量,不看独立用户**:`airi.product.events` 的 labels 必须保持低基数,不能加 `user_id`、`session_id`、request id、raw error message 或 prompt。要回答"每个功能有多少独立用户",查 Postgres `product_events`:`count(distinct user_id)`。
|
||||||
|
|
||||||
## Revenue & Billing
|
## Revenue & Billing
|
||||||
|
|
||||||
### Stripe lifecycle
|
### Stripe lifecycle
|
||||||
@@ -134,12 +142,15 @@ OTel SDK 在导出到 Prometheus 时做两件事:
|
|||||||
|
|
||||||
| Row | viz | 关键 metric |
|
| Row | viz | 关键 metric |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Service Health | stat / gauge | `user.active_sessions`(`max()`)、`ws.connections.active`(`sum()`)、`http.server.request.duration_count`(req/s + 5xx%)、`gen_ai.client.operation.count`、`airi.email.{send,failures}` 失败率 |
|
| Service Health | stat / gauge / heatmap | `user.total`(`max()`)、`user.active_sessions`(`avg()`)、`ws.connections.active`(`sum()`)、`http.server.request.duration_count`(req/s + 5xx%)、`gen_ai.client.operation.count` |
|
||||||
| Distribution (now) | donut | HTTP methods / LLM models / HTTP status codes — `increase([5m])` |
|
| User Engagement | stat | `user.active_rolling`(DAU / WAU / MAU,`max()`) |
|
||||||
| Traffic Trends | timeseries | 同 distribution 的数据 over time |
|
| Product Analytics | stat / gauge / bargauge / timeseries | `airi.product.events`(Prom-safe event volume by `feature` / `action` / `status`;distinct users 仍查 Postgres `product_events`) |
|
||||||
| Latency | timeseries | `http.server.request.duration_bucket`(P95 by route)、`gen_ai.client.first_token.duration_bucket`(P95 by model) |
|
| HTTP | heatmap / bargauge / timeseries | `http.server.request.duration_count`(status mix、top routes、route errors)、`http.server.request.duration_bucket`(P95 by route) |
|
||||||
| Errors / Quality | mix | 4xx/5xx stacked area、`airi.gen_ai.stream.interrupted`、`airi.rate_limit.blocked` |
|
| LLM Gateway | timeseries | `gen_ai.client.operation.count`(by model)、`gen_ai.client.first_token.duration_bucket` / `gen_ai.client.operation.duration_bucket`(TTFB + end-to-end P95) |
|
||||||
| Business | stat / gauge / donut | `airi.stripe.revenue`(by currency)、checkout conversion %、`stripe.events` 分布 |
|
| Provider Upstreams | timeseries | `gen_ai.client.operation.count` / `gen_ai.client.operation.duration_bucket` by `provider`、`airi.billing.tts.chars` |
|
||||||
|
| LLM Tokens & Quality | stat / timeseries | `gen_ai.client.token.usage.{input,output}`、`airi.billing.flux.unbilled`、`airi.gen_ai.stream.interrupted` |
|
||||||
|
| LLM Router Health | stat / gauge / timeseries | `airi.gen_ai.gateway.{key.exhausted,decrypt.failures,fallback.count,upstream.errors}` |
|
||||||
|
| Business | stat / gauge | `airi.stripe.revenue`(by currency)、checkout conversion %、`stripe.events` 分布 |
|
||||||
| Infrastructure (collapsed, **by `service_instance_id`**) | timeseries | `db_client_operation_duration` P95(cluster)、`db_client_connection_count`、`v8js_memory_heap_used_bytes` %、`nodejs_eventloop_delay_p99_seconds` |
|
| Infrastructure (collapsed, **by `service_instance_id`**) | timeseries | `db_client_operation_duration` P95(cluster)、`db_client_connection_count`、`v8js_memory_heap_used_bytes` %、`nodejs_eventloop_delay_p99_seconds` |
|
||||||
| Logs | logs | Loki,不是 Prometheus |
|
| Logs | logs | Loki,不是 Prometheus |
|
||||||
|
|
||||||
|
|||||||
@@ -904,6 +904,376 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"panel-95": {
|
||||||
|
"kind": "Panel",
|
||||||
|
"spec": {
|
||||||
|
"data": {
|
||||||
|
"kind": "QueryGroup",
|
||||||
|
"spec": {
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"kind": "PanelQuery",
|
||||||
|
"spec": {
|
||||||
|
"hidden": false,
|
||||||
|
"query": {
|
||||||
|
"datasource": {
|
||||||
|
"name": "grafanacloud-projairi-prom"
|
||||||
|
},
|
||||||
|
"group": "prometheus",
|
||||||
|
"kind": "DataQuery",
|
||||||
|
"spec": {
|
||||||
|
"expr": "sum(increase(airi_product_events_total{service_name=~\"$service\", deployment_environment=~\"$env\", feature!=\"\", action!=\"\"}[$__range]))",
|
||||||
|
"legendFormat": "events"
|
||||||
|
},
|
||||||
|
"version": "v0"
|
||||||
|
},
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"queryOptions": {},
|
||||||
|
"transformations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Total first-party product analytics events over the dashboard range. This is event volume, not distinct users — distinct-user counts come from the Postgres `product_events` table.",
|
||||||
|
"id": 95,
|
||||||
|
"links": [],
|
||||||
|
"title": "Product Events (range)",
|
||||||
|
"vizConfig": {
|
||||||
|
"group": "stat",
|
||||||
|
"kind": "VizConfig",
|
||||||
|
"spec": {
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "fixed",
|
||||||
|
"fixedColor": "blue"
|
||||||
|
},
|
||||||
|
"fieldMinMax": false,
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "blue",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short",
|
||||||
|
"noValue": "0"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"colorMode": "none",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"percentChangeColorMode": "standard",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showPercentChange": true,
|
||||||
|
"textMode": "value_and_name",
|
||||||
|
"wideLayout": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "13.0.0-23630096546"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"panel-96": {
|
||||||
|
"kind": "Panel",
|
||||||
|
"spec": {
|
||||||
|
"data": {
|
||||||
|
"kind": "QueryGroup",
|
||||||
|
"spec": {
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"kind": "PanelQuery",
|
||||||
|
"spec": {
|
||||||
|
"hidden": false,
|
||||||
|
"query": {
|
||||||
|
"datasource": {
|
||||||
|
"name": "grafanacloud-projairi-prom"
|
||||||
|
},
|
||||||
|
"group": "prometheus",
|
||||||
|
"kind": "DataQuery",
|
||||||
|
"spec": {
|
||||||
|
"expr": "100 * sum(increase(airi_product_events_total{service_name=~\"$service\", deployment_environment=~\"$env\", feature!=\"\", action!=\"\", status=\"failed\"}[$__range])) / clamp_min(sum(increase(airi_product_events_total{service_name=~\"$service\", deployment_environment=~\"$env\", feature!=\"\", action!=\"\"}[$__range])), 1)",
|
||||||
|
"legendFormat": "failed %"
|
||||||
|
},
|
||||||
|
"version": "v0"
|
||||||
|
},
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"queryOptions": {},
|
||||||
|
"transformations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Failed product events ÷ all product events over the dashboard range. Uses only bounded labels (`feature`, `action`, `status`, `source`); no user/session/request identifiers are present in Prometheus.",
|
||||||
|
"id": 96,
|
||||||
|
"links": [],
|
||||||
|
"title": "Product Failure %",
|
||||||
|
"vizConfig": {
|
||||||
|
"group": "gauge",
|
||||||
|
"kind": "VizConfig",
|
||||||
|
"spec": {
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"value": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "percent",
|
||||||
|
"decimals": 2,
|
||||||
|
"noValue": "0",
|
||||||
|
"min": 0,
|
||||||
|
"max": 20
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"minVizHeight": 75,
|
||||||
|
"minVizWidth": 75,
|
||||||
|
"orientation": "auto",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showThresholdLabels": false,
|
||||||
|
"showThresholdMarkers": true,
|
||||||
|
"sizing": "auto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "13.0.0-23630096546"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"panel-97": {
|
||||||
|
"kind": "Panel",
|
||||||
|
"spec": {
|
||||||
|
"data": {
|
||||||
|
"kind": "QueryGroup",
|
||||||
|
"spec": {
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"kind": "PanelQuery",
|
||||||
|
"spec": {
|
||||||
|
"hidden": false,
|
||||||
|
"query": {
|
||||||
|
"datasource": {
|
||||||
|
"name": "grafanacloud-projairi-prom"
|
||||||
|
},
|
||||||
|
"group": "prometheus",
|
||||||
|
"kind": "DataQuery",
|
||||||
|
"spec": {
|
||||||
|
"expr": "topk(12, sum by (feature, action, status) (increase(airi_product_events_total{service_name=~\"$service\", deployment_environment=~\"$env\", feature!=\"\", action!=\"\"}[$__range])))",
|
||||||
|
"legendFormat": "{{feature}} · {{action}} · {{status}}",
|
||||||
|
"instant": true,
|
||||||
|
"range": false
|
||||||
|
},
|
||||||
|
"version": "v0"
|
||||||
|
},
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"queryOptions": {},
|
||||||
|
"transformations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Top product actions by event count over the dashboard range. Use this to see which features are actually being exercised after deployment; pair with DB `count(distinct user_id)` for user counts.",
|
||||||
|
"id": 97,
|
||||||
|
"links": [],
|
||||||
|
"title": "Top Product Actions (range)",
|
||||||
|
"vizConfig": {
|
||||||
|
"group": "bargauge",
|
||||||
|
"kind": "VizConfig",
|
||||||
|
"spec": {
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "short",
|
||||||
|
"noValue": "0"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"displayMode": "gradient",
|
||||||
|
"maxVizHeight": 300,
|
||||||
|
"minVizHeight": 12,
|
||||||
|
"minVizWidth": 8,
|
||||||
|
"namePlacement": "auto",
|
||||||
|
"orientation": "horizontal",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showUnfilled": true,
|
||||||
|
"sizing": "auto",
|
||||||
|
"valueMode": "color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "13.0.0-23630096546"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"panel-98": {
|
||||||
|
"kind": "Panel",
|
||||||
|
"spec": {
|
||||||
|
"data": {
|
||||||
|
"kind": "QueryGroup",
|
||||||
|
"spec": {
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"kind": "PanelQuery",
|
||||||
|
"spec": {
|
||||||
|
"hidden": false,
|
||||||
|
"query": {
|
||||||
|
"datasource": {
|
||||||
|
"name": "grafanacloud-projairi-prom"
|
||||||
|
},
|
||||||
|
"group": "prometheus",
|
||||||
|
"kind": "DataQuery",
|
||||||
|
"spec": {
|
||||||
|
"expr": "sum by (feature, action, status) (rate(airi_product_events_total{service_name=~\"$service\", deployment_environment=~\"$env\", feature!=\"\", action!=\"\"}[$__rate_interval]))",
|
||||||
|
"legendFormat": "{{feature}} · {{action}} · {{status}}"
|
||||||
|
},
|
||||||
|
"version": "v0"
|
||||||
|
},
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"queryOptions": {},
|
||||||
|
"transformations": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Product event rate by feature/action/status. This is the Prometheus-safe trend view; user-level analysis remains in Postgres `product_events`.",
|
||||||
|
"id": 98,
|
||||||
|
"links": [],
|
||||||
|
"title": "Product Event Rate",
|
||||||
|
"vizConfig": {
|
||||||
|
"group": "timeseries",
|
||||||
|
"kind": "VizConfig",
|
||||||
|
"spec": {
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"barWidthFactor": 0.6,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 15,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "smooth",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "auto",
|
||||||
|
"showValues": false,
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "eps"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"annotations": {
|
||||||
|
"clustering": -1,
|
||||||
|
"multiLane": false
|
||||||
|
},
|
||||||
|
"legend": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull",
|
||||||
|
"max"
|
||||||
|
],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "right",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"hideZeros": false,
|
||||||
|
"mode": "multi",
|
||||||
|
"sort": "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"version": "13.0.0-23630096546"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"panel-16": {
|
"panel-16": {
|
||||||
"kind": "Panel",
|
"kind": "Panel",
|
||||||
"spec": {
|
"spec": {
|
||||||
@@ -3808,6 +4178,72 @@
|
|||||||
"title": "User Engagement"
|
"title": "User Engagement"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "RowsLayoutRow",
|
||||||
|
"spec": {
|
||||||
|
"collapse": false,
|
||||||
|
"layout": {
|
||||||
|
"kind": "GridLayout",
|
||||||
|
"spec": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"kind": "GridLayoutItem",
|
||||||
|
"spec": {
|
||||||
|
"element": {
|
||||||
|
"kind": "ElementReference",
|
||||||
|
"name": "panel-95"
|
||||||
|
},
|
||||||
|
"height": 5,
|
||||||
|
"width": 6,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "GridLayoutItem",
|
||||||
|
"spec": {
|
||||||
|
"element": {
|
||||||
|
"kind": "ElementReference",
|
||||||
|
"name": "panel-96"
|
||||||
|
},
|
||||||
|
"height": 5,
|
||||||
|
"width": 6,
|
||||||
|
"x": 6,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "GridLayoutItem",
|
||||||
|
"spec": {
|
||||||
|
"element": {
|
||||||
|
"kind": "ElementReference",
|
||||||
|
"name": "panel-97"
|
||||||
|
},
|
||||||
|
"height": 9,
|
||||||
|
"width": 12,
|
||||||
|
"x": 12,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "GridLayoutItem",
|
||||||
|
"spec": {
|
||||||
|
"element": {
|
||||||
|
"kind": "ElementReference",
|
||||||
|
"name": "panel-98"
|
||||||
|
},
|
||||||
|
"height": 4,
|
||||||
|
"width": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Product Analytics"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "RowsLayoutRow",
|
"kind": "RowsLayoutRow",
|
||||||
"spec": {
|
"spec": {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const SCHEMA_VERSION = '13.0.0-23630096546'
|
|||||||
// Service / env filter applied to every Prom query. Pulled into a helper so
|
// Service / env filter applied to every Prom query. Pulled into a helper so
|
||||||
// the variable name only appears once.
|
// the variable name only appears once.
|
||||||
const SERVICE_FILTER = 'service_name=~"$service", deployment_environment=~"$env"'
|
const SERVICE_FILTER = 'service_name=~"$service", deployment_environment=~"$env"'
|
||||||
|
const PRODUCT_EVENT_FILTER = `${SERVICE_FILTER}, feature!="", action!=""`
|
||||||
|
|
||||||
// Build-script local types. Kept loose — Grafana owns the schema, and we
|
// Build-script local types. Kept loose — Grafana owns the schema, and we
|
||||||
// validate the rendered JSON by re-importing it into Grafana, not by typing.
|
// validate the rendered JSON by re-importing it into Grafana, not by typing.
|
||||||
@@ -526,6 +527,56 @@ elements['panel-92'] = timeseriesPanel(
|
|||||||
{ unit: 'short', fillOpacity: 30 },
|
{ unit: 'short', fillOpacity: 30 },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// --- Product Analytics — event-volume view only ---------------------------
|
||||||
|
// Prometheus deliberately does not carry user_id. These panels answer
|
||||||
|
// "which product actions are happening and failing"; DB-side product_events
|
||||||
|
// queries answer "how many distinct users used each feature".
|
||||||
|
elements['panel-95'] = statPanel(
|
||||||
|
95,
|
||||||
|
'Product Events (range)',
|
||||||
|
'Total first-party product analytics events over the dashboard range. This is event volume, not distinct users — distinct-user counts come from the Postgres `product_events` table.',
|
||||||
|
[
|
||||||
|
query(`sum(increase(airi_product_events_total{${PRODUCT_EVENT_FILTER}}[$__range]))`, 'events'),
|
||||||
|
],
|
||||||
|
{ unit: 'short', variant: 'count', noValue: '0', graphMode: 'none' },
|
||||||
|
)
|
||||||
|
|
||||||
|
elements['panel-96'] = gaugePanel(
|
||||||
|
96,
|
||||||
|
'Product Failure %',
|
||||||
|
'Failed product events ÷ all product events over the dashboard range. Uses only bounded labels (`feature`, `action`, `status`, `source`); no user/session/request identifiers are present in Prometheus.',
|
||||||
|
[query(
|
||||||
|
`100 * sum(increase(airi_product_events_total{${PRODUCT_EVENT_FILTER}, status="failed"}[$__range])) / clamp_min(sum(increase(airi_product_events_total{${PRODUCT_EVENT_FILTER}}[$__range])), 1)`,
|
||||||
|
'failed %',
|
||||||
|
)],
|
||||||
|
{ steps: [{ color: 'green', value: 0 }, { color: 'yellow', value: 2 }, { color: 'red', value: 10 }], max: 20, decimals: 2, noValue: '0' },
|
||||||
|
)
|
||||||
|
|
||||||
|
elements['panel-97'] = barGaugePanel(
|
||||||
|
97,
|
||||||
|
'Top Product Actions (range)',
|
||||||
|
'Top product actions by event count over the dashboard range. Use this to see which features are actually being exercised after deployment; pair with DB `count(distinct user_id)` for user counts.',
|
||||||
|
[query(
|
||||||
|
`topk(12, sum by (feature, action, status) (increase(airi_product_events_total{${PRODUCT_EVENT_FILTER}}[$__range])))`,
|
||||||
|
'{{feature}} · {{action}} · {{status}}',
|
||||||
|
'A',
|
||||||
|
PROM,
|
||||||
|
{ instant: true },
|
||||||
|
)],
|
||||||
|
{ unit: 'short', noValue: '0' },
|
||||||
|
)
|
||||||
|
|
||||||
|
elements['panel-98'] = timeseriesPanel(
|
||||||
|
98,
|
||||||
|
'Product Event Rate',
|
||||||
|
'Product event rate by feature/action/status. This is the Prometheus-safe trend view; user-level analysis remains in Postgres `product_events`.',
|
||||||
|
[query(
|
||||||
|
`sum by (feature, action, status) (rate(airi_product_events_total{${PRODUCT_EVENT_FILTER}}[$__rate_interval]))`,
|
||||||
|
'{{feature}} · {{action}} · {{status}}',
|
||||||
|
)],
|
||||||
|
{ unit: 'eps', fillOpacity: 15 },
|
||||||
|
)
|
||||||
|
|
||||||
// --- Row 2: HTTP — traffic ranking, error trend, latency trend -------------
|
// --- Row 2: HTTP — traffic ranking, error trend, latency trend -------------
|
||||||
elements['panel-16'] = barGaugePanel(
|
elements['panel-16'] = barGaugePanel(
|
||||||
16,
|
16,
|
||||||
@@ -857,6 +908,15 @@ const rows = [
|
|||||||
item('panel-81', 8, 0, 8, 4),
|
item('panel-81', 8, 0, 8, 4),
|
||||||
item('panel-82', 16, 0, 8, 4),
|
item('panel-82', 16, 0, 8, 4),
|
||||||
]),
|
]),
|
||||||
|
// Row 3: Product Analytics — Prom-safe event volume and failure trend.
|
||||||
|
// Distinct-user analytics stay in Postgres `product_events`; this row
|
||||||
|
// intentionally never uses user_id/session/request labels.
|
||||||
|
row('Product Analytics', [
|
||||||
|
item('panel-95', 0, 0, 6, 5),
|
||||||
|
item('panel-96', 6, 0, 6, 5),
|
||||||
|
item('panel-97', 12, 0, 12, 9),
|
||||||
|
item('panel-98', 0, 5, 12, 4),
|
||||||
|
]),
|
||||||
// Row 3: HTTP — full-width error breakdown on top, then traffic ranking +
|
// Row 3: HTTP — full-width error breakdown on top, then traffic ranking +
|
||||||
// latency trend side by side.
|
// latency trend side by side.
|
||||||
row('HTTP', [
|
row('HTTP', [
|
||||||
@@ -979,14 +1039,15 @@ const variables = [
|
|||||||
* 1. Service Health — signup/sessions/WS counts, req-rate, 5xx, status-code
|
* 1. Service Health — signup/sessions/WS counts, req-rate, 5xx, status-code
|
||||||
* heatmap, live WS trend: "is anything broken right now?"
|
* heatmap, live WS trend: "is anything broken right now?"
|
||||||
* 2. User Engagement — rolling DAU/WAU/MAU from user.last_seen_at
|
* 2. User Engagement — rolling DAU/WAU/MAU from user.last_seen_at
|
||||||
* 3. HTTP — error breakdown by route, request ranking, latency by route
|
* 3. Product Analytics — Prom-safe product event volume + failure trend
|
||||||
* 4. LLM Gateway — per-model request rate + latency (TTFB + end-to-end)
|
* 4. HTTP — error breakdown by route, request ranking, latency by route
|
||||||
* 5. Provider Upstreams — per-provider rate/latency/failure + TTS chars
|
* 5. LLM Gateway — per-model request rate + latency (TTFB + end-to-end)
|
||||||
* 6. LLM Tokens & Quality — token totals/throughput, revenue-leak alerts
|
* 6. Provider Upstreams — per-provider rate/latency/failure + TTS chars
|
||||||
* 7. LLM Router Health — key/decrypt/fallback "wake someone up" signals
|
* 7. LLM Tokens & Quality — token totals/throughput, revenue-leak alerts
|
||||||
* 8. Business — Stripe / Flux money flow
|
* 8. LLM Router Health — key/decrypt/fallback "wake someone up" signals
|
||||||
* 9. Infrastructure (collapsed) — DB / runtime health for triage
|
* 9. Business — Stripe / Flux money flow
|
||||||
* 10. Logs — Loki for live debugging
|
* 10. Infrastructure (collapsed) — DB / runtime health for triage
|
||||||
|
* 11. Logs — Loki for live debugging
|
||||||
*
|
*
|
||||||
* One metric, one panel: we deliberately do not duplicate a metric across
|
* One metric, one panel: we deliberately do not duplicate a metric across
|
||||||
* stat/trend/bar/pie forms. Counter conventions: rate() for "now" trends,
|
* stat/trend/bar/pie forms. Counter conventions: rate() for "now" trends,
|
||||||
|
|||||||
Reference in New Issue
Block a user