diff --git a/.env.example b/.env.example
index 6ba6eb2..dcc9002 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,23 @@
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api/
-PORT=3000
\ No newline at end of file
+PORT=3000
+# Next App FRONTEND Instrumentation
+NEXT_PUBLIC_FARO_URL=http://localhost:12347/collect
+NEXT_PUBLIC_FARO_APP_NAME=next-frontend
+NEXT_PUBLIC_FARO_APP_NAMESPACE=nextjs-example
+NEXT_PUBLIC_FARO_APP_VERSION=1.0.0
+NEXT_PUBLIC_APP_ENV=development
+
+# Next App BACKEND Instrumentation
+## Example assumes that the collector is running on the same machine
+OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
+## Force protobuf
+OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
+## Set Backend service name
+OTEL_SERVICE_NAME=next-backend
+## Customize resource attributes, namespace is a recommended attribute
+OTEL_RESOURCE_ATTRIBUTES=service.namespace=nextjs-example
+
+# OTel collector
+GRAFANA_CLOUD_USERNAME=
+GRAFANA_CLOUD_API_KEY=
+GRAFANA_CLOUD_ENDPOINT=
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 768d126..97e4035 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,11 +1,13 @@
import 'app/styles/global.css';
import { QueryProvider } from 'shared/providers';
import { Toaster, TooltipProvider } from 'shared/ui';
+import FrontendObservability from 'shared/config/metrics/FrontendObservability';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
+
{children}
diff --git a/infra/observability/.env.example b/infra/observability/.env.example
new file mode 100644
index 0000000..b477aed
--- /dev/null
+++ b/infra/observability/.env.example
@@ -0,0 +1,18 @@
+# Grafana
+GRAFANA_PORT=3010
+GRAFANA_ADMIN_USER=admin
+GRAFANA_ADMIN_PASSWORD=admin
+
+# Loki
+LOKI_HTTP_PORT=3100
+LOKI_GRPC_PORT=9096
+
+# Tempo
+TEMPO_PORT=3200
+TEMPO_OTLP_GRPC_PORT=4317
+TEMPO_OTLP_HTTP_PORT=4318
+
+# Alloy
+ALLOY_UI_PORT=12345
+ALLOY_FARO_PORT=12347
+ALLOY_OTLP_GRPC_PORT=4319
diff --git a/infra/observability/README.md b/infra/observability/README.md
new file mode 100644
index 0000000..99847ad
--- /dev/null
+++ b/infra/observability/README.md
@@ -0,0 +1,59 @@
+## Observability stack
+
+Локальный стек для логов, трейсов и frontend observability:
+
+- Grafana
+- Loki
+- Tempo
+- Grafana Alloy
+
+## Запуск
+
+Из корня репозитория.
+
+Создайте локальный env-файл:
+
+```bash
+cp ./infra/observability/.env.example ./infra/observability/.env
+```
+
+Запустите стек:
+
+```bash
+docker compose -f ./infra/observability/compose.observability.yaml up -d
+```
+
+Остановить стек:
+
+```bash
+docker compose -f ./infra/observability/compose.observability.yaml down
+```
+
+Проверить статус контейнеров:
+
+```bash
+docker compose -f ./infra/observability/compose.observability.yaml ps
+```
+
+## Адреса
+
+- Grafana: `http://localhost:3010`
+- Loki: `http://localhost:3100`
+- Tempo: `http://localhost:3200`
+- Alloy UI: `http://localhost:12345`
+- Faro endpoint: `http://localhost:12347/collect`
+
+Логин Grafana по умолчанию: `admin` / `admin`.
+
+Порты и credentials можно переопределить в `infra/observability/.env`.
+
+## Дашборды
+
+Grafana автоматически подхватывает provisioned dashboards из `infra/observability/grafana/dashboards`.
+
+Основные дашборды:
+
+- `Task Tracker / Frontend`
+- `Task Tracker / Frontend Traces (Tempo)`
+
+Если в Grafana пустой список дашбордов, проверьте, что стек поднят командой из корня репозитория. Это важно для корректных bind mounts из `infra/observability/grafana/*`.
diff --git a/infra/observability/alloy/config.alloy b/infra/observability/alloy/config.alloy
new file mode 100644
index 0000000..0820889
--- /dev/null
+++ b/infra/observability/alloy/config.alloy
@@ -0,0 +1,97 @@
+discovery.docker "docker_containers" {
+ host = "unix:///var/run/docker.sock"
+}
+
+discovery.relabel "docker_containers" {
+ targets = discovery.docker.docker_containers.targets
+
+ rule {
+ source_labels = ["__meta_docker_container_name"]
+ target_label = "container"
+ }
+}
+
+loki.source.docker "docker_logs" {
+ host = "unix:///var/run/docker.sock"
+ targets = discovery.relabel.docker_containers.output
+ forward_to = [loki.process.docker_logs.receiver]
+}
+
+loki.process "docker_logs" {
+ stage.docker {}
+
+ forward_to = [loki.write.local_loki.receiver]
+}
+
+loki.process "frontend_faro_logs" {
+ stage.logfmt {
+ mapping = {
+ app_name = "",
+ kind = "",
+ session_id = "",
+ browser_name = "",
+ browser_version = "",
+ os_name = "",
+ screen_width = "",
+ screen_height = "",
+ }
+ }
+
+ stage.template {
+ source = "screen_resolution"
+ template = "{{ if and .screen_width .screen_height }}{{ .screen_width }}x{{ .screen_height }}{{ end }}"
+ }
+
+ stage.labels {
+ values = {
+ app = "app_name",
+ kind = "kind",
+ session_id = "session_id",
+ browser_name = "browser_name",
+ browser_version = "browser_version",
+ os_name = "os_name",
+ screen_resolution = "screen_resolution",
+ }
+ }
+
+ forward_to = [loki.write.local_loki.receiver]
+}
+
+loki.write "local_loki" {
+ endpoint {
+ url = "http://" + sys.env("LOKI_HOST") + ":" + sys.env("LOKI_HTTP_PORT") + "/loki/api/v1/push"
+ }
+}
+
+faro.receiver "frontend" {
+ server {
+ listen_address = "0.0.0.0"
+ listen_port = sys.env("ALLOY_FARO_PORT")
+ cors_allowed_origins = ["*"]
+ }
+
+ output {
+ logs = [loki.process.frontend_faro_logs.receiver]
+ traces = [otelcol.exporter.otlp.tempo.input]
+ }
+}
+
+otelcol.receiver.otlp "ingest" {
+ grpc {
+ endpoint = "0.0.0.0:" + sys.env("ALLOY_OTLP_GRPC_PORT")
+ }
+
+ output {
+ traces = [otelcol.exporter.otlp.tempo.input]
+ }
+}
+
+otelcol.exporter.otlp "tempo" {
+ client {
+ endpoint = sys.env("TEMPO_HOST") + ":" + sys.env("TEMPO_OTLP_GRPC_PORT")
+
+ tls {
+ insecure = true
+ }
+ }
+}
diff --git a/infra/observability/compose.observability.yaml b/infra/observability/compose.observability.yaml
new file mode 100644
index 0000000..d54f8c4
--- /dev/null
+++ b/infra/observability/compose.observability.yaml
@@ -0,0 +1,102 @@
+name: task-tracker-observability
+
+services:
+ grafana:
+ image: grafana/grafana-enterprise:latest
+ container_name: grafana
+ env_file:
+ - .env
+ environment:
+ GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
+ GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}
+ ports:
+ - '${GRAFANA_PORT:-3010}:3000'
+ volumes:
+ - grafana_data:/var/lib/grafana
+ - ./grafana/provisioning:/etc/grafana/provisioning
+ - ./grafana/dashboards:/etc/grafana/dashboards
+ depends_on:
+ - loki
+ - tempo
+ networks:
+ - observability
+
+ loki:
+ image: grafana/loki:latest
+ container_name: loki
+ command:
+ - -config.file=/etc/loki/local-config.yaml
+ - -config.expand-env=true
+ env_file:
+ - .env
+ environment:
+ LOKI_HTTP_PORT: ${LOKI_HTTP_PORT:-3100}
+ LOKI_GRPC_PORT: ${LOKI_GRPC_PORT:-9096}
+ ports:
+ - '${LOKI_HTTP_PORT:-3100}:3100'
+ - '${LOKI_GRPC_PORT:-9096}:9096'
+ volumes:
+ - ./loki/loki-config.yaml:/etc/loki/local-config.yaml
+ - loki_data:/loki
+ networks:
+ - observability
+
+ tempo:
+ image: grafana/tempo:latest
+ container_name: tempo
+ command:
+ - -config.file=/etc/tempo/tempo.yaml
+ - -config.expand-env=true
+ env_file:
+ - .env
+ environment:
+ TEMPO_PORT: ${TEMPO_PORT:-3200}
+ TEMPO_OTLP_GRPC_PORT: ${TEMPO_OTLP_GRPC_PORT:-4317}
+ TEMPO_OTLP_HTTP_PORT: ${TEMPO_OTLP_HTTP_PORT:-4318}
+ ports:
+ - '${TEMPO_PORT:-3200}:3200'
+ - '${TEMPO_OTLP_GRPC_PORT:-4317}:4317'
+ - '${TEMPO_OTLP_HTTP_PORT:-4318}:4318'
+ volumes:
+ - ./tempo/tempo-config.yaml:/etc/tempo/tempo.yaml
+ - tempo_data:/var/tempo
+ networks:
+ - observability
+
+ alloy:
+ image: grafana/alloy:latest
+ container_name: alloy
+ env_file:
+ - .env
+ environment:
+ LOKI_HOST: loki
+ LOKI_HTTP_PORT: ${LOKI_HTTP_PORT:-3100}
+ TEMPO_HOST: tempo
+ TEMPO_OTLP_GRPC_PORT: ${TEMPO_OTLP_GRPC_PORT:-4317}
+ ALLOY_OTLP_GRPC_PORT: ${ALLOY_OTLP_GRPC_PORT:-4319}
+ command:
+ - run
+ - --server.http.listen-addr=0.0.0.0:${ALLOY_UI_PORT:-12345}
+ - --storage.path=/var/lib/alloy/data
+ - /etc/alloy/config.alloy
+ ports:
+ - '${ALLOY_UI_PORT:-12345}:${ALLOY_UI_PORT:-12345}'
+ - '${ALLOY_FARO_PORT:-12347}:12347'
+ - '${ALLOY_OTLP_GRPC_PORT:-4319}:4319'
+ volumes:
+ - ./alloy/config.alloy:/etc/alloy/config.alloy
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ depends_on:
+ - loki
+ - tempo
+ networks:
+ - observability
+
+volumes:
+ grafana_data:
+ loki_data:
+ tempo_data:
+
+networks:
+ observability:
+ name: task-tracker-observability
diff --git a/infra/observability/grafana/dashboards/frontend-observability.json b/infra/observability/grafana/dashboards/frontend-observability.json
new file mode 100644
index 0000000..dd90c25
--- /dev/null
+++ b/infra/observability/grafana/dashboards/frontend-observability.json
@@ -0,0 +1,1248 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "target": {
+ "limit": 100,
+ "matchAny": false,
+ "tags": [],
+ "type": "dashboard"
+ },
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "iteration": 1655814818965,
+ "links": [],
+ "liveNow": false,
+ "panels": [
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 14,
+ "panels": [],
+ "title": "Performance",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 600
+ }
+ ]
+ },
+ "unit": "ms"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 3,
+ "w": 3,
+ "x": 0,
+ "y": 1
+ },
+ "id": 8,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "9.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "avg_over_time({kind=\"measurement\",app=\"$app\"} |= \"ttfb\" | logfmt | unwrap ttfb [$__range]) by (app)",
+ "legendFormat": "TTFB",
+ "queryType": "range",
+ "refId": "A"
+ }
+ ],
+ "title": "TTFB",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "#EAB839",
+ "value": 1800
+ },
+ {
+ "color": "red",
+ "value": 3000
+ }
+ ]
+ },
+ "unit": "ms"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 3,
+ "w": 3,
+ "x": 3,
+ "y": 1
+ },
+ "id": 9,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "9.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "avg_over_time({kind=\"measurement\",app=\"$app\"} |= \" fcp=\" | logfmt | unwrap fcp [$__range]) by (app)",
+ "legendFormat": "FCP",
+ "queryType": "range",
+ "refId": "A"
+ }
+ ],
+ "title": "FCP",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "max": 1,
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "#EAB839",
+ "value": 0.1
+ },
+ {
+ "color": "red",
+ "value": 0.3
+ }
+ ]
+ },
+ "unit": "none"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 3,
+ "w": 3,
+ "x": 6,
+ "y": 1
+ },
+ "id": 11,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "9.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "avg_over_time({kind=\"measurement\",app=\"$app\"} |= \"cls\" | logfmt | unwrap cls [$__range]) by (app)",
+ "legendFormat": "CLS",
+ "queryType": "range",
+ "refId": "A"
+ }
+ ],
+ "title": "CLS",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "#EAB839",
+ "value": 2500
+ },
+ {
+ "color": "red",
+ "value": 4000
+ }
+ ]
+ },
+ "unit": "ms"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 3,
+ "w": 3,
+ "x": 9,
+ "y": 1
+ },
+ "id": 10,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "9.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "avg_over_time({kind=\"measurement\",app=\"$app\"} |= \" lcp=\" | logfmt | unwrap lcp [$__range]) by (app)",
+ "legendFormat": "LCP",
+ "queryType": "range",
+ "refId": "A"
+ }
+ ],
+ "title": "LCP",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "normal"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "ms"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 8,
+ "x": 0,
+ "y": 4
+ },
+ "id": 2,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "quantile_over_time(0.75, {kind=\"measurement\",app=\"$app\"} |= \" ttfb=\" | logfmt | unwrap ttfb [5m]) by (app)",
+ "legendFormat": "TTFB",
+ "queryType": "range",
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "quantile_over_time(0.75, {kind=\"measurement\",app=\"$app\"} |= \" fcp=\" | logfmt | unwrap fcp [5m]) by (app)",
+ "hide": false,
+ "legendFormat": "FCP",
+ "queryType": "range",
+ "refId": "B"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "quantile_over_time(0.75, {kind=\"measurement\",app=\"$app\"} |= \" lcp=\" | logfmt | unwrap lcp [5m]) by (app)",
+ "hide": false,
+ "legendFormat": "LCP",
+ "queryType": "range",
+ "refId": "C"
+ }
+ ],
+ "title": "Page Load, p75",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "line"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "#EAB839",
+ "value": 0.1
+ },
+ {
+ "color": "red",
+ "value": 0.25
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 7,
+ "x": 8,
+ "y": 4
+ },
+ "id": 4,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "quantile_over_time(0.75, {kind=\"measurement\",app=\"$app\"} |= \" cls=\" | logfmt | unwrap cls [5m]) by (app)",
+ "legendFormat": "CLS",
+ "queryType": "range",
+ "refId": "A"
+ }
+ ],
+ "title": "Cumulative Layout Shift, p75",
+ "type": "timeseries"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 12
+ },
+ "id": 16,
+ "panels": [],
+ "title": "Exceptions",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 1
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 4,
+ "w": 2,
+ "x": 0,
+ "y": 13
+ },
+ "id": 18,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "none",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": ["lastNotNull"],
+ "fields": "",
+ "values": false
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "9.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "expr": "sum(count_over_time({app=\"$app\",kind=\"exception\"} [$__range])) by (app)",
+ "refId": "A"
+ }
+ ],
+ "title": "Total Exceptions",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "errors"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "red",
+ "mode": "fixed"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 2,
+ "y": 13
+ },
+ "id": 25,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "count_over_time({app=\"$app\",kind=\"exception\"} [$__interval])",
+ "legendFormat": "errors",
+ "queryType": "range",
+ "refId": "A"
+ }
+ ],
+ "title": "Exceptions Over Time",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {
+ "align": "auto",
+ "displayMode": "auto",
+ "inspect": false
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Count"
+ },
+ "properties": [
+ {
+ "id": "custom.width",
+ "value": 100
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 8,
+ "x": 14,
+ "y": 13
+ },
+ "id": 20,
+ "options": {
+ "footer": {
+ "fields": "",
+ "reducer": ["sum"],
+ "show": false
+ },
+ "showHeader": true,
+ "sortBy": [
+ {
+ "desc": true,
+ "displayName": "Count"
+ }
+ ]
+ },
+ "pluginVersion": "9.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "topk(10, count(count_over_time({kind=\"exception\", app=\"$app\"} | logfmt [$__range])) by (value))",
+ "queryType": "instant",
+ "refId": "A"
+ }
+ ],
+ "title": "Top Exceptions",
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true
+ },
+ "indexByName": {},
+ "renameByName": {
+ "Value #A": "Count",
+ "value": "Error"
+ }
+ }
+ }
+ ],
+ "type": "table"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {
+ "align": "auto",
+ "displayMode": "auto",
+ "inspect": false
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Count"
+ },
+ "properties": [
+ {
+ "id": "custom.width",
+ "value": 100
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 9,
+ "w": 14,
+ "x": 0,
+ "y": 21
+ },
+ "id": 21,
+ "options": {
+ "footer": {
+ "fields": "",
+ "reducer": ["sum"],
+ "show": false
+ },
+ "showHeader": true,
+ "sortBy": [
+ {
+ "desc": true,
+ "displayName": "Count"
+ }
+ ]
+ },
+ "pluginVersion": "9.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "topk(10, count(count_over_time({kind=\"exception\", app=\"$app\"} | logfmt [$__range])) by (page_url))",
+ "queryType": "instant",
+ "refId": "A"
+ }
+ ],
+ "title": "Top URLs by Exception Count",
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true
+ },
+ "indexByName": {},
+ "renameByName": {
+ "Value #A": "Count",
+ "page_url": "URL",
+ "value": "Error"
+ }
+ }
+ }
+ ],
+ "type": "table"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {
+ "align": "auto",
+ "displayMode": "auto",
+ "inspect": false
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Version"
+ },
+ "properties": [
+ {
+ "id": "custom.width",
+ "value": 150
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Errors"
+ },
+ "properties": [
+ {
+ "id": "custom.width",
+ "value": 100
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 9,
+ "w": 8,
+ "x": 14,
+ "y": 21
+ },
+ "id": 23,
+ "options": {
+ "footer": {
+ "fields": "",
+ "reducer": ["sum"],
+ "show": false
+ },
+ "showHeader": true,
+ "sortBy": [
+ {
+ "desc": true,
+ "displayName": "Count"
+ }
+ ]
+ },
+ "pluginVersion": "9.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "topk(10, count by (browser_name, browser_version) (count_over_time({kind=\"exception\", app=\"$app\"} | logfmt [$__range])))",
+ "queryType": "instant",
+ "refId": "A"
+ }
+ ],
+ "title": "Top Browsers by Exception Count",
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true
+ },
+ "indexByName": {},
+ "renameByName": {
+ "Value #A": "Count",
+ "browser_name": "Browser",
+ "browser_version": "Version",
+ "page_url": "URL",
+ "value": "Error"
+ }
+ }
+ }
+ ],
+ "type": "table"
+ },
+ {
+ "collapsed": false,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 30
+ },
+ "id": 27,
+ "panels": [],
+ "title": "Meta",
+ "type": "row"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {
+ "align": "auto",
+ "displayMode": "auto",
+ "inspect": false
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 10,
+ "w": 10,
+ "x": 0,
+ "y": 31
+ },
+ "id": 29,
+ "options": {
+ "footer": {
+ "fields": "",
+ "reducer": ["sum"],
+ "show": false
+ },
+ "showHeader": true,
+ "sortBy": [
+ {
+ "desc": true,
+ "displayName": "Count"
+ }
+ ]
+ },
+ "pluginVersion": "9.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "topk(10, count(count_over_time({app=\"$app\",kind=\"measurement\"} |= \" ttfb=\" | logfmt [$__range])) by (browser_name, browser_version))",
+ "queryType": "instant",
+ "refId": "A"
+ }
+ ],
+ "title": "Popular Browsers",
+ "transformations": [
+ {
+ "id": "organize",
+ "options": {
+ "excludeByName": {
+ "Time": true
+ },
+ "indexByName": {},
+ "renameByName": {
+ "Value #A": "Count",
+ "browser_name": "Browser",
+ "browser_version": "Version"
+ }
+ }
+ }
+ ],
+ "type": "table"
+ },
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "errors"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "fixedColor": "red",
+ "mode": "fixed"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 10,
+ "w": 12,
+ "x": 10,
+ "y": 31
+ },
+ "id": 30,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "editorMode": "code",
+ "expr": "count_over_time({app=\"$app\",kind=\"measurement\"} |= \" ttfb=\" [$__interval])",
+ "legendFormat": "visits",
+ "queryType": "range",
+ "refId": "A"
+ }
+ ],
+ "title": "Visits",
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "",
+ "schemaVersion": 36,
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": [
+ {
+ "current": {},
+ "datasource": {
+ "type": "loki",
+ "uid": "loki"
+ },
+ "definition": "label_values(app)",
+ "hide": 0,
+ "includeAll": false,
+ "label": "app",
+ "multi": false,
+ "name": "app",
+ "options": [],
+ "query": "label_values(app)",
+ "refresh": 1,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "type": "query"
+ }
+ ]
+ },
+ "time": {
+ "from": "now-6h",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Frontend",
+ "version": 13,
+ "weekStart": ""
+}
diff --git a/infra/observability/grafana/dashboards/frontend-traces-tempo.json b/infra/observability/grafana/dashboards/frontend-traces-tempo.json
new file mode 100644
index 0000000..b645197
--- /dev/null
+++ b/infra/observability/grafana/dashboards/frontend-traces-tempo.json
@@ -0,0 +1,123 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "datasource": {
+ "type": "tempo",
+ "uid": "tempo"
+ },
+ "gridPos": {
+ "h": 10,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 2,
+ "targets": [
+ {
+ "datasource": {
+ "type": "tempo",
+ "uid": "tempo"
+ },
+ "query": "{ resource.service.name =~ \"$service_name\" }",
+ "queryType": "traceqlSearch",
+ "refId": "A"
+ }
+ ],
+ "title": "Trace Search (Tempo)",
+ "type": "table"
+ },
+ {
+ "gridPos": {
+ "h": 12,
+ "w": 24,
+ "x": 0,
+ "y": 10
+ },
+ "id": 1,
+ "options": {
+ "code": {
+ "language": "markdown",
+ "showLineNumbers": false,
+ "showMiniMap": false
+ },
+ "content": "## Tempo quick queries for frontend\n\nDatasource: `Tempo`\nService variable: `$service_name`\n\n- Errors:\n```traceql\n{ resource.service.name =~ \"$service_name\" && status = error }\n```\n\n- Slow traces (>500ms):\n```traceql\n{ resource.service.name =~ \"$service_name\" && duration > 500ms }\n```\n\n- All frontend traces:\n```traceql\n{ resource.service.name =~ \"$service_name\" }\n```\n\nTip: set time range to last 15m/1h and sort by duration.",
+ "mode": "markdown"
+ },
+ "pluginVersion": "12.2.0",
+ "title": "Tempo TraceQL Cheat Sheet",
+ "type": "text"
+ }
+ ],
+ "preload": false,
+ "refresh": "10s",
+ "schemaVersion": 40,
+ "tags": ["task-tracker", "frontend", "tempo", "traces"],
+ "templating": {
+ "list": [
+ {
+ "current": {
+ "selected": true,
+ "text": "All",
+ "value": "$__all"
+ },
+ "hide": 0,
+ "includeAll": true,
+ "label": "service_name",
+ "multi": false,
+ "name": "service_name",
+ "options": [
+ {
+ "selected": true,
+ "text": "All",
+ "value": "$__all"
+ },
+ {
+ "selected": false,
+ "text": "ttopen-backend",
+ "value": "ttopen-backend"
+ },
+ {
+ "selected": false,
+ "text": "ttopen-frontend",
+ "value": "ttopen-frontend"
+ }
+ ],
+ "query": "ttopen-frontend",
+ "skipUrlSync": false,
+ "type": "custom",
+ "allValue": ".*"
+ }
+ ]
+ },
+ "time": {
+ "from": "now-1h",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Frontend Traces (Tempo)",
+ "uid": "task-tracker-frontend-traces-tempo",
+ "version": 1,
+ "weekStart": ""
+}
diff --git a/infra/observability/grafana/provisioning/dashboards/dashboards.yaml b/infra/observability/grafana/provisioning/dashboards/dashboards.yaml
new file mode 100644
index 0000000..ef617d5
--- /dev/null
+++ b/infra/observability/grafana/provisioning/dashboards/dashboards.yaml
@@ -0,0 +1,11 @@
+apiVersion: 1
+
+providers:
+ - name: task-tracker-observability
+ orgId: 1
+ folder: Task Tracker
+ type: file
+ disableDeletion: false
+ editable: true
+ options:
+ path: /etc/grafana/dashboards
diff --git a/infra/observability/grafana/provisioning/datasources/datasources.yaml b/infra/observability/grafana/provisioning/datasources/datasources.yaml
new file mode 100644
index 0000000..3891e7f
--- /dev/null
+++ b/infra/observability/grafana/provisioning/datasources/datasources.yaml
@@ -0,0 +1,23 @@
+apiVersion: 1
+
+deleteDatasources:
+ - name: Loki
+ orgId: 1
+ - name: Tempo
+ orgId: 1
+
+datasources:
+ - name: Loki
+ uid: loki
+ type: loki
+ access: proxy
+ url: http://loki:3100
+ isDefault: true
+ editable: true
+
+ - name: Tempo
+ uid: tempo
+ type: tempo
+ access: proxy
+ url: http://tempo:3200
+ editable: true
diff --git a/infra/observability/loki/loki-config.yaml b/infra/observability/loki/loki-config.yaml
new file mode 100644
index 0000000..b6b653e
--- /dev/null
+++ b/infra/observability/loki/loki-config.yaml
@@ -0,0 +1,26 @@
+auth_enabled: false
+
+server:
+ http_listen_port: ${LOKI_HTTP_PORT:-3100}
+ grpc_listen_port: ${LOKI_GRPC_PORT:-9096}
+
+common:
+ path_prefix: /loki
+ storage:
+ filesystem:
+ chunks_directory: /loki/chunks
+ rules_directory: /loki/rules
+ replication_factor: 1
+ ring:
+ kvstore:
+ store: inmemory
+
+schema_config:
+ configs:
+ - from: 2020-10-24
+ store: tsdb
+ object_store: filesystem
+ schema: v13
+ index:
+ prefix: index_
+ period: 24h
diff --git a/infra/observability/tempo/tempo-config.yaml b/infra/observability/tempo/tempo-config.yaml
new file mode 100644
index 0000000..3893dcc
--- /dev/null
+++ b/infra/observability/tempo/tempo-config.yaml
@@ -0,0 +1,24 @@
+stream_over_http_enabled: true
+
+server:
+ http_listen_port: ${TEMPO_PORT:-3200}
+
+distributor:
+ receivers:
+ otlp:
+ protocols:
+ grpc:
+ endpoint: '0.0.0.0:${TEMPO_OTLP_GRPC_PORT:-4317}'
+ http:
+ endpoint: '0.0.0.0:${TEMPO_OTLP_HTTP_PORT:-4318}'
+
+storage:
+ trace:
+ backend: local
+ wal:
+ path: /var/tempo/wal
+ local:
+ path: /var/tempo/blocks
+
+usage_report:
+ reporting_enabled: false
diff --git a/instrumentation.ts b/instrumentation.ts
new file mode 100644
index 0000000..4197ab2
--- /dev/null
+++ b/instrumentation.ts
@@ -0,0 +1,31 @@
+import { Span, SpanProcessor } from '@opentelemetry/sdk-trace-node';
+import { registerOTel } from '@vercel/otel';
+
+/**
+ * Span processor to reduce cardinality of span names.
+ *
+ * Customize with care!
+ */
+class SpanNameProcessor implements SpanProcessor {
+ forceFlush(): Promise {
+ return Promise.resolve();
+ }
+ onStart(span: Span): void {
+ if (span.name.startsWith('GET /_next/static')) {
+ span.updateName('GET /_next/static');
+ } else if (span.name.startsWith('GET /_next/data')) {
+ span.updateName('GET /_next/data');
+ }
+ }
+ onEnd(): void {}
+ shutdown(): Promise {
+ return Promise.resolve();
+ }
+}
+
+export function register() {
+ registerOTel({
+ serviceName: process.env.OTEL_SERVICE_NAME || 'unknown_service:node',
+ spanProcessors: ['auto', new SpanNameProcessor()],
+ });
+}
diff --git a/package.json b/package.json
index 2514572..51f5c36 100644
--- a/package.json
+++ b/package.json
@@ -23,9 +23,14 @@
"prepare": "husky"
},
"dependencies": {
+ "@grafana/faro-web-sdk": "^2.4.0",
+ "@grafana/faro-web-tracing": "^2.4.0",
"@hookform/resolvers": "^5.2.2",
+ "@opentelemetry/api": "^1.9.1",
+ "@opentelemetry/sdk-trace-node": "^2.7.1",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-query-devtools": "^5.91.3",
+ "@vercel/otel": "^2.1.2",
"axios": "^1.15.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6376411..97ccd71 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,15 +11,30 @@ importers:
.:
dependencies:
+ '@grafana/faro-web-sdk':
+ specifier: ^2.4.0
+ version: 2.4.0
+ '@grafana/faro-web-tracing':
+ specifier: ^2.4.0
+ version: 2.4.0
'@hookform/resolvers':
specifier: ^5.2.2
version: 5.2.2(react-hook-form@7.72.1(react@19.2.5))
+ '@opentelemetry/api':
+ specifier: ^1.9.1
+ version: 1.9.1
+ '@opentelemetry/sdk-trace-node':
+ specifier: ^2.7.1
+ version: 2.7.1(@opentelemetry/api@1.9.1)
'@tanstack/react-query':
specifier: ^5.90.21
version: 5.99.0(react@19.2.5)
'@tanstack/react-query-devtools':
specifier: ^5.91.3
version: 5.99.0(@tanstack/react-query@5.99.0(react@19.2.5))(react@19.2.5)
+ '@vercel/otel':
+ specifier: ^2.1.2
+ version: 2.1.2(@opentelemetry/api-logs@0.216.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.216.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.216.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.7.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))
axios:
specifier: ^1.15.0
version: 1.15.0
@@ -37,7 +52,7 @@ importers:
version: 0.574.0(react@19.2.5)
next:
specifier: ^16.1.6
- version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ version: 16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -77,7 +92,7 @@ importers:
version: 0.5.7(typescript@5.9.3)
'@storybook/nextjs-vite':
specifier: ^10.3.3
- version: 10.3.5(@babel/core@7.29.0)(esbuild@0.27.7)(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))
+ version: 10.3.5(@babel/core@7.29.0)(esbuild@0.27.7)(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))
'@tailwindcss/postcss':
specifier: ^4.2.0
version: 4.2.2
@@ -170,7 +185,7 @@ importers:
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3)
vitest:
specifier: ^4.1.2
- version: 4.1.4(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(msw@2.13.6(@types/node@25.6.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))
+ version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(msw@2.13.6(@types/node@25.6.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))
packages:
@@ -622,6 +637,15 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+ '@grafana/faro-core@2.4.0':
+ resolution: {integrity: sha512-rE+A2Mk1TLvVs+huCX45VWR0DMkxdFkAM9yi6cKVUZhq2dAdpNjSOHy/0ubiUa23hmyVNL3ZYfN+RWUbTgaYBQ==}
+
+ '@grafana/faro-web-sdk@2.4.0':
+ resolution: {integrity: sha512-n6y+bWGvrfQiG7tjTXA9sAhsvCogN3J/zS3cXV5QtbkgZHp737qtZ9q3Mh1MIKnr+3dlxgMkh7i56TlG+xc+kA==}
+
+ '@grafana/faro-web-tracing@2.4.0':
+ resolution: {integrity: sha512-AUyhAI6FGpf5MP1iUpbZh+JmjF4+tRU8fLh21BTvbFkDNIqyWNFZlEtixcpks1WTBtIQlpUKbz7fxcJvlSLWaA==}
+
'@hono/node-server@1.19.14':
resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==}
engines: {node: '>=18.14.1'}
@@ -986,6 +1010,142 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
+ '@opentelemetry/api-logs@0.215.0':
+ resolution: {integrity: sha512-xrFlqhdhUyO8wSRn6DjE0145/HPWSJ5Nm0C7vWua6TdL/FSEAZvEyvdsa9CRXuxo9ebb7j/NEPhEcO62IJ0qUA==}
+ engines: {node: '>=8.0.0'}
+
+ '@opentelemetry/api-logs@0.216.0':
+ resolution: {integrity: sha512-KmGTgvxTJ0J01d4mOeX1wMV5NUTNf9HebIuOOGDfIn0a/IrnXIQbOnlylDyl9tkDv4h0DUpdI/GqCdLzfTkUXg==}
+ engines: {node: '>=8.0.0'}
+
+ '@opentelemetry/api@1.9.1':
+ resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
+ engines: {node: '>=8.0.0'}
+
+ '@opentelemetry/context-async-hooks@2.7.1':
+ resolution: {integrity: sha512-OPFBYuXEn1E4ja3Y6eeA7O+ZnLBNcXTV5Cgsn1VaqBZ6hC5FnpZPLBNme1LJY8ZtF4aOujPKFoeWN4ik487KuQ==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
+ '@opentelemetry/core@2.7.0':
+ resolution: {integrity: sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
+ '@opentelemetry/core@2.7.1':
+ resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
+ '@opentelemetry/exporter-trace-otlp-http@0.215.0':
+ resolution: {integrity: sha512-k4J9ISeGpb0Bm/wCrlcrbroMFTkiWMrdhNxQGrlktxLy127Yzd4/7nrTawn5d/ApktYTknvdixsE6++34Qfi1w==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/instrumentation-fetch@0.215.0':
+ resolution: {integrity: sha512-ljaUeeF5CB7RNaUn8f/uZddNigmlYGeZvXpKl8boa3upTYLOHtBlMFNAKJyO+h1lt1Im9Y1cgA30gVE64iHvCw==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/instrumentation-xml-http-request@0.215.0':
+ resolution: {integrity: sha512-z8//4beDua4OQ0MOvix2aVi5YUw3ybRb2QjOlI4wUJvzqptC9wCDt64itE+uRA/MBMeoGgKX+z8aBcQcuOybSA==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/instrumentation@0.215.0':
+ resolution: {integrity: sha512-SyJONuqypQ2xWdYMy99vF7JhZ2kDTGx4oRmM/jZV+kRtZ96JTnJmEINbIJgHz7Gnhtw0bimHwbPy/pguA5wpPQ==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/instrumentation@0.216.0':
+ resolution: {integrity: sha512-BrY0b2K81OLgwBcFxY2wKgPFhq4DpindT+S83++zquc5Rtb2SuYLMkujgDRWMgZQDz+OT+dfvPnMGADPuw4FDw==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/otlp-exporter-base@0.215.0':
+ resolution: {integrity: sha512-lHrfbmeLSmesGSkkHiqDwOzfaEMSWXdc7q6UoLfbW8byONCb+bE/zkAr0kapN4US1baT/2nbpNT7Cn9XoB96Vg==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/otlp-transformer@0.215.0':
+ resolution: {integrity: sha512-cWwBvaV+vkXHkSoTYR8hGw+AW03UlgTr6xtrUKOMeum3T+8vffYXIfXu6KY5MLu8O9QtoBKqaKWw9I5xoOepng==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': ^1.3.0
+
+ '@opentelemetry/resources@2.7.0':
+ resolution: {integrity: sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.3.0 <1.10.0'
+
+ '@opentelemetry/resources@2.7.1':
+ resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.3.0 <1.10.0'
+
+ '@opentelemetry/sdk-logs@0.215.0':
+ resolution: {integrity: sha512-y3ucOmphzc4vgBTyIGchs+N/1rkACmoka8QalT2z1LBNM232Z17zMYayHcMl+dgMoOadZ0b72UZv7mDtqy1cFA==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.4.0 <1.10.0'
+
+ '@opentelemetry/sdk-logs@0.216.0':
+ resolution: {integrity: sha512-KB3rcwQuitq0JbbsCcNdqMhRJX3kArAYz/ovb0jGRaBQAIrt2roik3xQXuhYxS37zx0jSkUZcJu1z3Y2UCxbDA==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.4.0 <1.10.0'
+
+ '@opentelemetry/sdk-metrics@2.7.0':
+ resolution: {integrity: sha512-Vd7h95av/LYRsAVN7wbprvvJnHkq7swMXAo7Uad0Uxf9jl6NSReLa0JNivrcc5BVIx/vl2t+cgdVQQbnVhsR9w==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.9.0 <1.10.0'
+
+ '@opentelemetry/sdk-trace-base@2.7.0':
+ resolution: {integrity: sha512-Yg9zEXJB50DLVLpsKPk7NmNqlPlS+OvqhJGh0A8oawIOTPOwlm4eXs9BMJV7L79lvEwI+dWtAj+YjTyddV336A==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.3.0 <1.10.0'
+
+ '@opentelemetry/sdk-trace-base@2.7.1':
+ resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.3.0 <1.10.0'
+
+ '@opentelemetry/sdk-trace-node@2.7.1':
+ resolution: {integrity: sha512-pCpQxU68lV+I9s9svqMyVu5iHdDDUnqUpSxqwyCU8A9ejEsSnMPCbearwsUO4yk08ZJzAIUCFuReMdVQvHrdvg==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
+ '@opentelemetry/sdk-trace-web@2.7.0':
+ resolution: {integrity: sha512-WehQSom/hQO0uDVtYQV5O+UaTQU6UFMevYs0uE33bK/4abEyRHrIZF+3DGMmTaz08jQkCfaa0xTOpR873WKn1g==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
+ '@opentelemetry/sdk-trace-web@2.7.1':
+ resolution: {integrity: sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.0.0 <1.10.0'
+
+ '@opentelemetry/semantic-conventions@1.40.0':
+ resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==}
+ engines: {node: '>=14'}
+
'@oxc-project/types@0.124.0':
resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==}
@@ -2258,6 +2418,18 @@ packages:
cpu: [x64]
os: [win32]
+ '@vercel/otel@2.1.2':
+ resolution: {integrity: sha512-PbGyq1lLwWbnftylNcQ6KFxc7DLc8LJdnaU3snM43bgXiWkWsHrgaU/LdUD8sHaSgG8UKuydX/aymNt3J+hzuA==}
+ engines: {node: ^18.19.0 || >=20.6.0}
+ peerDependencies:
+ '@opentelemetry/api': '>=1.9.0 <2.0.0'
+ '@opentelemetry/api-logs': '>=0.200.0 <0.300.0'
+ '@opentelemetry/instrumentation': '>=0.200.0 <0.300.0'
+ '@opentelemetry/resources': '>=2.0.0 <3.0.0'
+ '@opentelemetry/sdk-logs': '>=0.200.0 <0.300.0'
+ '@opentelemetry/sdk-metrics': '>=2.0.0 <3.0.0'
+ '@opentelemetry/sdk-trace-base': '>=2.0.0 <3.0.0'
+
'@vitejs/plugin-react@6.0.1':
resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2334,6 +2506,11 @@ packages:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
+ acorn-import-attributes@1.9.5:
+ resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
+ peerDependencies:
+ acorn: ^8
+
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -2555,6 +2732,9 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
+ cjs-module-lexer@2.2.0:
+ resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==}
+
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@@ -3463,6 +3643,10 @@ packages:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
+ import-in-the-middle@3.0.1:
+ resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==}
+ engines: {node: '>=18'}
+
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
@@ -3874,6 +4058,9 @@ packages:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
engines: {node: '>=18'}
+ long@5.3.2:
+ resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
+
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -3976,6 +4163,9 @@ packages:
engines: {node: '>=18'}
hasBin: true
+ module-details-from-path@1.0.4:
+ resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
+
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -4342,6 +4532,10 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ protobufjs@8.0.3:
+ resolution: {integrity: sha512-LBYnMWkKLB8fE/ljROPDbCl7mgLSlI+oBe1fAAr5MTqFg4TIi0tYrVVurJvQggOjnUYMQtEZBjrej59ojMNTHQ==}
+ engines: {node: '>=12.0.0'}
+
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@@ -4473,6 +4667,10 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
+ require-in-the-middle@8.0.1:
+ resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
+ engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -4931,6 +5129,10 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ ua-parser-js@1.0.41:
+ resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
+ hasBin: true
+
unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
@@ -5116,6 +5318,9 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
+ web-vitals@5.2.0:
+ resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==}
+
webidl-conversions@8.0.1:
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
engines: {node: '>=20'}
@@ -5743,6 +5948,33 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
+ '@grafana/faro-core@2.4.0':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/otlp-transformer': 0.215.0(@opentelemetry/api@1.9.1)
+
+ '@grafana/faro-web-sdk@2.4.0':
+ dependencies:
+ '@grafana/faro-core': 2.4.0
+ ua-parser-js: 1.0.41
+ web-vitals: 5.2.0
+
+ '@grafana/faro-web-tracing@2.4.0':
+ dependencies:
+ '@grafana/faro-web-sdk': 2.4.0
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/exporter-trace-otlp-http': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/instrumentation': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/instrumentation-fetch': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/instrumentation-xml-http-request': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/otlp-transformer': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.40.0
+ transitivePeerDependencies:
+ - supports-color
+
'@hono/node-server@1.19.14(hono@4.12.15)':
dependencies:
hono: 4.12.15
@@ -6024,6 +6256,163 @@ snapshots:
'@open-draft/until@2.1.0': {}
+ '@opentelemetry/api-logs@0.215.0':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+
+ '@opentelemetry/api-logs@0.216.0':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+
+ '@opentelemetry/api@1.9.1': {}
+
+ '@opentelemetry/context-async-hooks@2.7.1(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+
+ '@opentelemetry/core@2.7.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/semantic-conventions': 1.40.0
+
+ '@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/semantic-conventions': 1.40.0
+
+ '@opentelemetry/exporter-trace-otlp-http@0.215.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/otlp-exporter-base': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/otlp-transformer': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-base': 2.7.0(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/instrumentation-fetch@0.215.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/instrumentation': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-web': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.40.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@opentelemetry/instrumentation-xml-http-request@0.215.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/instrumentation': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-web': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.40.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@opentelemetry/instrumentation@0.215.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.215.0
+ import-in-the-middle: 3.0.1
+ require-in-the-middle: 8.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@opentelemetry/instrumentation@0.216.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.216.0
+ import-in-the-middle: 3.0.1
+ require-in-the-middle: 8.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@opentelemetry/otlp-exporter-base@0.215.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/otlp-transformer': 0.215.0(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/otlp-transformer@0.215.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.215.0
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-logs': 0.215.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-metrics': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-base': 2.7.0(@opentelemetry/api@1.9.1)
+ protobufjs: 8.0.3
+
+ '@opentelemetry/resources@2.7.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.40.0
+
+ '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.40.0
+
+ '@opentelemetry/sdk-logs@0.215.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.215.0
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.40.0
+
+ '@opentelemetry/sdk-logs@0.216.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.216.0
+ '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.40.0
+
+ '@opentelemetry/sdk-metrics@2.7.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.0(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/sdk-trace-base@2.7.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.40.0
+
+ '@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/semantic-conventions': 1.40.0
+
+ '@opentelemetry/sdk-trace-node@2.7.1(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/context-async-hooks': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/sdk-trace-web@2.7.0(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-base': 2.7.0(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/sdk-trace-web@2.7.1(@opentelemetry/api@1.9.1)':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
+
+ '@opentelemetry/semantic-conventions@1.40.0': {}
+
'@oxc-project/types@0.124.0': {}
'@radix-ui/number@1.1.1': {}
@@ -6872,18 +7261,18 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(esbuild@0.27.7)(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))':
+ '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(esbuild@0.27.7)(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))':
dependencies:
'@storybook/builder-vite': 10.3.5(esbuild@0.27.7)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))
'@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)
'@storybook/react-vite': 10.3.5(esbuild@0.27.7)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))
- next: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ next: 16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5)
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3)
- vite-plugin-storybook-nextjs: 3.2.4(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))
+ vite-plugin-storybook-nextjs: 3.2.4(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
@@ -7288,6 +7677,16 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
+ '@vercel/otel@2.1.2(@opentelemetry/api-logs@0.216.0)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.216.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.216.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@2.7.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))':
+ dependencies:
+ '@opentelemetry/api': 1.9.1
+ '@opentelemetry/api-logs': 0.216.0
+ '@opentelemetry/instrumentation': 0.216.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-logs': 0.216.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-metrics': 2.7.0(@opentelemetry/api@1.9.1)
+ '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
+
'@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-rc.7
@@ -7396,6 +7795,10 @@ snapshots:
mime-types: 3.0.2
negotiator: 1.0.0
+ acorn-import-attributes@1.9.5(acorn@8.16.0):
+ dependencies:
+ acorn: 8.16.0
+
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
@@ -7642,6 +8045,8 @@ snapshots:
dependencies:
readdirp: 4.1.2
+ cjs-module-lexer@2.2.0: {}
+
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@@ -8706,6 +9111,13 @@ snapshots:
parent-module: 1.0.1
resolve-from: 4.0.0
+ import-in-the-middle@3.0.1:
+ dependencies:
+ acorn: 8.16.0
+ acorn-import-attributes: 1.9.5(acorn@8.16.0)
+ cjs-module-lexer: 2.2.0
+ module-details-from-path: 1.0.4
+
imurmurhash@0.1.4: {}
indent-string@4.0.0: {}
@@ -9078,6 +9490,8 @@ snapshots:
strip-ansi: 7.2.0
wrap-ansi: 9.0.2
+ long@5.3.2: {}
+
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
@@ -9154,6 +9568,8 @@ snapshots:
ast-module-types: 6.0.1
node-source-walk: 7.0.1
+ module-details-from-path@1.0.4: {}
+
ms@2.1.3: {}
msw@2.13.6(@types/node@25.6.0)(typescript@5.9.3):
@@ -9196,7 +9612,7 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
- next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+ next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@next/env': 16.2.3
'@swc/helpers': 0.5.15
@@ -9215,6 +9631,7 @@ snapshots:
'@next/swc-linux-x64-musl': 16.2.3
'@next/swc-win32-arm64-msvc': 16.2.3
'@next/swc-win32-x64-msvc': 16.2.3
+ '@opentelemetry/api': 1.9.1
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -9496,6 +9913,11 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
+ protobufjs@8.0.3:
+ dependencies:
+ '@types/node': 25.6.0
+ long: 5.3.2
+
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@@ -9685,6 +10107,13 @@ snapshots:
require-from-string@2.0.2: {}
+ require-in-the-middle@8.0.1:
+ dependencies:
+ debug: 4.4.3
+ module-details-from-path: 1.0.4
+ transitivePeerDependencies:
+ - supports-color
+
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -10304,6 +10733,8 @@ snapshots:
typescript@5.9.3: {}
+ ua-parser-js@1.0.41: {}
+
unbox-primitive@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -10389,13 +10820,13 @@ snapshots:
vary@1.1.2: {}
- vite-plugin-storybook-nextjs@3.2.4(next@16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3)):
+ vite-plugin-storybook-nextjs@3.2.4(next@16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3)):
dependencies:
'@next/env': 16.0.0
image-size: 2.0.2
magic-string: 0.30.21
module-alias: 2.3.4
- next: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ next: 16.2.3(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
ts-dedent: 2.2.0
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3)
@@ -10429,7 +10860,7 @@ snapshots:
jiti: 2.6.1
yaml: 2.8.3
- vitest@4.1.4(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(msw@2.13.6(@types/node@25.6.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3)):
+ vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(msw@2.13.6(@types/node@25.6.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3)):
dependencies:
'@vitest/expect': 4.1.4
'@vitest/mocker': 4.1.4(msw@2.13.6(@types/node@25.6.0)(typescript@5.9.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3))
@@ -10452,6 +10883,7 @@ snapshots:
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(yaml@2.8.3)
why-is-node-running: 2.3.0
optionalDependencies:
+ '@opentelemetry/api': 1.9.1
'@types/node': 25.6.0
jsdom: 29.0.2(@noble/hashes@1.8.0)
transitivePeerDependencies:
@@ -10463,6 +10895,8 @@ snapshots:
web-streams-polyfill@3.3.3: {}
+ web-vitals@5.2.0: {}
+
webidl-conversions@8.0.1: {}
webpack-virtual-modules@0.6.2: {}
diff --git a/proxy.ts b/proxy.ts
index eefe91b..4ce29c5 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -2,6 +2,7 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { routes } from 'shared/config';
import type { Route } from 'next';
+import { trace } from '@opentelemetry/api';
const REFRESH_COOKIE = 'refresh';
@@ -28,7 +29,18 @@ export function proxy(req: NextRequest) {
return NextResponse.redirect(new URL(routes.profile(), req.url));
}
- return NextResponse.next();
+ const response = NextResponse.next();
+ const current = trace.getActiveSpan();
+
+ // set server-timing header with traceparent
+ if (current) {
+ response.headers.set(
+ 'server-timing',
+ `traceparent;desc="00-${current.spanContext().traceId}-${current.spanContext().spanId}-01"`
+ );
+ }
+
+ return response;
}
export const config = {
diff --git a/src/shared/config/metrics/FrontendObservability.tsx b/src/shared/config/metrics/FrontendObservability.tsx
new file mode 100644
index 0000000..4ea4b6a
--- /dev/null
+++ b/src/shared/config/metrics/FrontendObservability.tsx
@@ -0,0 +1,73 @@
+'use client';
+
+import { useEffect } from 'react';
+import { faro, getWebInstrumentations, initializeFaro } from '@grafana/faro-web-sdk';
+import { TracingInstrumentation } from '@grafana/faro-web-tracing';
+
+let isFaroInitialized = false;
+
+export default function FrontendObservability() {
+ useEffect(() => {
+ const initializeWhenIdle = () => {
+ if (isFaroInitialized || faro.api) {
+ return;
+ }
+
+ const faroUrl = process.env.NEXT_PUBLIC_FARO_URL;
+ const appName = process.env.NEXT_PUBLIC_FARO_APP_NAME;
+
+ if (!faroUrl || !appName) {
+ return;
+ }
+
+ const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
+ const traceHeaderCorsUrls = apiBaseUrl ? [new URL(apiBaseUrl).origin] : [];
+
+ try {
+ initializeFaro({
+ url: faroUrl,
+ // Basic metadata allows filtering by env/release in Grafana Explore.
+ app: {
+ name: appName,
+ namespace: process.env.NEXT_PUBLIC_FARO_APP_NAMESPACE || undefined,
+ version: process.env.NEXT_PUBLIC_FARO_APP_VERSION || '1.0.0',
+ environment: process.env.NEXT_PUBLIC_APP_ENV || process.env.NODE_ENV || 'development',
+ },
+ instrumentations: [
+ ...getWebInstrumentations(),
+ new TracingInstrumentation({
+ instrumentationOptions: {
+ propagateTraceHeaderCorsUrls: traceHeaderCorsUrls,
+ },
+ }),
+ ],
+ });
+
+ isFaroInitialized = true;
+ } catch {
+ // Silent fail: observability should never break user flows.
+ }
+ };
+
+ let idleCallbackId: number | null = null;
+ let timeoutId: ReturnType | null = null;
+
+ if ('requestIdleCallback' in window) {
+ idleCallbackId = window.requestIdleCallback(() => initializeWhenIdle(), { timeout: 2000 });
+ } else {
+ timeoutId = setTimeout(initializeWhenIdle, 0);
+ }
+
+ return () => {
+ if (idleCallbackId !== null && 'cancelIdleCallback' in window) {
+ window.cancelIdleCallback(idleCallbackId);
+ }
+
+ if (timeoutId !== null) {
+ clearTimeout(timeoutId);
+ }
+ };
+ }, []);
+
+ return null;
+}