diff --git a/ml/serving/logging_config.py b/ml/serving/logging_config.py new file mode 100644 index 0000000..b40ae40 --- /dev/null +++ b/ml/serving/logging_config.py @@ -0,0 +1,20 @@ +"""Structlog JSON configuration — import once at process start.""" +import logging +import structlog + + +def configure() -> None: + structlog.configure( + processors=[ + structlog.contextvars.merge_contextvars, + structlog.stdlib.add_log_level, + structlog.stdlib.add_logger_name, + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.JSONRenderer(), + ], + wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), + context_class=dict, + logger_factory=structlog.PrintLoggerFactory(), + ) + logging.basicConfig(level=logging.WARNING) diff --git a/ml/serving/main.py b/ml/serving/main.py index f03e5fc..111eb2b 100644 --- a/ml/serving/main.py +++ b/ml/serving/main.py @@ -34,12 +34,25 @@ from typing import Optional, Deque import httpx import numpy as np -from fastapi import FastAPI, HTTPException +import sentry_sdk +import structlog +import structlog.contextvars +from fastapi import FastAPI, HTTPException, Request from pydantic import BaseModel +from starlette.middleware.base import BaseHTTPMiddleware +import logging_config import nats_consumer from prompts import get_prompt +logging_config.configure() + +_SENTRY_DSN = os.getenv("SENTRY_DSN") +if _SENTRY_DSN: + sentry_sdk.init(dsn=_SENTRY_DSN, environment=os.getenv("ENV", "development")) + +log = structlog.get_logger() + @asynccontextmanager async def lifespan(app: FastAPI): @@ -50,6 +63,21 @@ async def lifespan(app: FastAPI): app = FastAPI(title="oO ML Serving", version="1.0.0", lifespan=lifespan) + +class _TracingMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + structlog.contextvars.clear_contextvars() + traceparent = request.headers.get("traceparent", "") + if traceparent: + parts = traceparent.split("-") + trace_id = parts[1] if len(parts) == 4 and len(parts[1]) == 32 else None + if trace_id: + structlog.contextvars.bind_contextvars(trace_id=trace_id) + return await call_next(request) + + +app.add_middleware(_TracingMiddleware) + LITELLM_URL = os.getenv("LITELLM_URL", "http://localhost:4000") LITELLM_MASTER_KEY = os.getenv("LITELLM_MASTER_KEY", "sk-oo-dev") diff --git a/ml/serving/nats_consumer.py b/ml/serving/nats_consumer.py index 8aa4d0f..3fb0f03 100644 --- a/ml/serving/nats_consumer.py +++ b/ml/serving/nats_consumer.py @@ -17,15 +17,15 @@ Config (env vars): from __future__ import annotations import json -import logging import os import time from pathlib import Path from typing import Optional +import structlog from schemas import TaskSyncedPayload, TipFeedbackPayload -logger = logging.getLogger(__name__) +log = structlog.get_logger(__name__) NATS_URL = os.getenv("NATS_URL", "") NATS_DURABLE_PREFIX = os.getenv("NATS_DURABLE_PREFIX", "feature-pipeline") @@ -56,15 +56,12 @@ async def _handle(subject: str, payload: dict, state_dir: Path) -> None: "last_sync_ts": msg.syncedAt, "task_count": msg.count, })) - logger.info("[nats] task_synced user=%s count=%s", msg.userId, msg.count) + log.info("nats: task_synced", user_id=msg.userId, count=msg.count) elif subject == "signals.tip.feedback": msg = TipFeedbackPayload.model_validate(payload) - logger.info( - "[nats] tip_feedback user=%s tip=%s action=%s reward=%s", - msg.userId, msg.tipId, msg.action, msg.reward, - ) + log.info("nats: tip_feedback", user_id=msg.userId, tip_id=msg.tipId, action=msg.action, reward=msg.reward) else: - logger.debug("[nats] unhandled subject=%s", subject) + log.debug("nats: unhandled subject", subject=subject) # ── Consumer factory ─────────────────────────────────────────────────────── @@ -80,7 +77,7 @@ def _make_handler(key: str, state_dir: Path): consumer_health[key]["processed"] += 1 except Exception as exc: consumer_health[key]["errors"] += 1 - logger.warning("[nats] processing error key=%s subject=%s: %s", key, msg.subject, exc) + log.warning("nats: processing error", key=key, subject=msg.subject, exc=str(exc)) await msg.nak() return handler @@ -91,7 +88,7 @@ async def start(state_dir: Path) -> None: """Connect to NATS and register durable push consumers. No-op if NATS_URL is unset.""" global _nc if not NATS_URL: - logger.info("[nats] NATS_URL unset — JetStream consumers disabled") + log.info("nats: NATS_URL unset — JetStream consumers disabled") return try: @@ -105,9 +102,9 @@ async def start(state_dir: Path) -> None: max_reconnect_attempts=-1, ) js = _nc.jetstream() - logger.info("[nats] connected to %s", NATS_URL) + log.info("nats: connected", url=NATS_URL) except Exception as exc: - logger.warning("[nats] connection failed: %s — consumers disabled", exc) + log.warning("nats: connection failed — consumers disabled", exc=str(exc)) _nc = None return @@ -126,9 +123,9 @@ async def start(state_dir: Path) -> None: config=config, ) _subs.append(sub) - logger.info("[nats] subscribed subject=%s durable=%s", subject, durable) + log.info("nats: subscribed", subject=subject, durable=durable) except Exception as exc: - logger.warning("[nats] subscribe failed key=%s: %s", key, exc) + log.warning("nats: subscribe failed", key=key, exc=str(exc)) async def stop() -> None: @@ -146,4 +143,4 @@ async def stop() -> None: except Exception: pass _nc = None - logger.info("[nats] disconnected") + log.info("nats: disconnected") diff --git a/ml/serving/requirements.txt b/ml/serving/requirements.txt index af17243..4d142cc 100644 --- a/ml/serving/requirements.txt +++ b/ml/serving/requirements.txt @@ -5,3 +5,5 @@ numpy>=1.26.0 httpx>=0.27.0 anthropic>=0.40.0 nats-py>=2.9.0 +structlog>=24.1.0 +sentry-sdk>=2.0.0 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c97b708..a47cb25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,7 +34,7 @@ importers: version: 14.1.4 next: specifier: ^15.1.6 - version: 15.5.15(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 15.5.15(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: specifier: ^19.0.0 version: 19.2.5 @@ -74,7 +74,7 @@ importers: version: link:../../packages/shared-types next: specifier: ^15.1.6 - version: 15.5.15(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 15.5.15(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: specifier: ^19.0.0 version: 19.2.5 @@ -117,7 +117,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) + version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) ml/serving: {} @@ -131,13 +131,16 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) + version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) services/api: dependencies: '@oo/shared-types': specifier: workspace:* version: link:../../packages/shared-types + '@sentry/node': + specifier: ^10.50.0 + version: 10.50.0 better-sqlite3: specifier: ^11.8.1 version: 11.10.0 @@ -152,7 +155,7 @@ importers: version: 16.6.1 drizzle-orm: specifier: ^0.38.3 - version: 0.38.4(@types/better-sqlite3@7.6.13)(@types/react@19.2.14)(better-sqlite3@11.10.0)(react@19.2.5) + version: 0.38.4(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(@types/react@19.2.14)(better-sqlite3@11.10.0)(react@19.2.5) express: specifier: ^4.21.2 version: 4.22.1 @@ -171,6 +174,12 @@ importers: openid-client: specifier: ^6.3.4 version: 6.8.3 + pino: + specifier: ^10.3.1 + version: 10.3.1 + pino-http: + specifier: ^11.0.0 + version: 11.0.0 web-push: specifier: ^3.6.7 version: 3.6.7 @@ -210,7 +219,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)) + version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)) packages: @@ -756,6 +765,11 @@ packages: '@noble/hashes': optional: true + '@fastify/otel@0.18.0': + resolution: {integrity: sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -1044,17 +1058,217 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.207.0': + resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.212.0': + resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.214.0': + resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} + 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/core@2.6.1': + resolution: {integrity: sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==} + 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/instrumentation-amqplib@0.61.0': + resolution: {integrity: sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.57.0': + resolution: {integrity: sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dataloader@0.31.0': + resolution: {integrity: sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.33.0': + resolution: {integrity: sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.57.0': + resolution: {integrity: sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.62.0': + resolution: {integrity: sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.60.0': + resolution: {integrity: sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.214.0': + resolution: {integrity: sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.62.0': + resolution: {integrity: sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.23.0': + resolution: {integrity: sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.58.0': + resolution: {integrity: sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.62.0': + resolution: {integrity: sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.58.0': + resolution: {integrity: sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.67.0': + resolution: {integrity: sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.60.0': + resolution: {integrity: sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.60.0': + resolution: {integrity: sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.60.0': + resolution: {integrity: sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.66.0': + resolution: {integrity: sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.62.0': + resolution: {integrity: sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.33.0': + resolution: {integrity: sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.207.0': + resolution: {integrity: sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.212.0': + resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.214.0': + resolution: {integrity: sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/redis-common@0.38.3': + resolution: {integrity: sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==} + engines: {node: ^18.19.0 || >=20.6.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/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/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.41.2': + resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} '@petamoriken/float16@3.9.3': resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@playwright/test@1.59.1': resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} hasBin: true + '@prisma/instrumentation@7.6.0': + resolution: {integrity: sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ==} + peerDependencies: + '@opentelemetry/api': ^1.8 + '@react-aria/focus@3.22.0': resolution: {integrity: sha512-ZfDOVuVhqDsM9mkNji3QUZ/d40JhlVgXrDkrfXylM1035QCrcTHN7m2DpbE95sU2A8EQb4wikvt5jM6K/73BPg==} peerDependencies: @@ -1173,6 +1387,47 @@ packages: '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@sentry/core@10.50.0': + resolution: {integrity: sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg==} + engines: {node: '>=18'} + + '@sentry/node-core@10.50.0': + resolution: {integrity: sha512-Eb1BYf4Lc7ZYmdX3acKP6SgyGikrBA370gbGHaWI5jRu7G7vig8sIu1ghPmY5AlvqBPOetado7GniXr6fAXbTw==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + '@opentelemetry/semantic-conventions': + optional: true + + '@sentry/node@10.50.0': + resolution: {integrity: sha512-TvwzFQu8MGKzMQ2/tqxcNzFA8UG2kKTB+GDmA4uOzx3+GT849YZRRSJzEXCmYhk1teVd2fbmgqyYY2nyLF5a+Q==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@10.50.0': + resolution: {integrity: sha512-axn3pgDPveGdaMUC0abMCmFN7ux2pA5ebPufCef4lMIsyg7BBQvaEJ+vE19wjstMaBCAJGsdZlL3eeP2rtgRMw==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1335,9 +1590,18 @@ packages: '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node@22.19.17': resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + '@types/pg-pool@2.0.7': + resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} + + '@types/pg@8.15.6': + resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/qs@6.15.0': resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} @@ -1358,6 +1622,9 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/web-push@3.6.4': resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} @@ -1416,6 +1683,16 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -1462,6 +1739,10 @@ packages: ast-v8-to-istanbul@1.0.0: resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + autoprefixer@10.5.0: resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} @@ -1469,6 +1750,10 @@ packages: peerDependencies: postcss: ^8.1.0 + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1500,6 +1785,10 @@ packages: resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1548,6 +1837,9 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -1955,6 +2247,9 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1987,6 +2282,10 @@ packages: engines: {node: '>= 18.0.0'} hasBin: true + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2051,6 +2350,13 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + + import-in-the-middle@3.0.1: + resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} + engines: {node: '>=18'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -2297,12 +2603,19 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -2396,6 +2709,10 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2426,6 +2743,17 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2441,6 +2769,19 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-http@11.0.0: + resolution: {integrity: sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -2506,6 +2847,22 @@ packages: resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -2516,6 +2873,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2537,6 +2897,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + random-bytes@1.0.0: resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} engines: {node: '>= 0.8'} @@ -2617,6 +2980,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + recharts-scale@0.4.5: resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} @@ -2635,6 +3002,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-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -2658,6 +3029,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2717,6 +3092,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2728,6 +3106,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -2803,6 +3185,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -3043,6 +3429,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -3362,6 +3752,16 @@ snapshots: '@exodus/bytes@1.15.0': {} + '@fastify/otel@0.18.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.212.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + '@floating-ui/core@1.7.5': dependencies: '@floating-ui/utils': 0.2.11 @@ -3578,14 +3978,269 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@opentelemetry/api-logs@0.207.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.212.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.214.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/core@2.7.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/instrumentation-amqplib@0.61.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.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.57.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.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.31.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.33.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.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.57.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.60.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.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.38.3 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.23.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.58.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.62.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.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.58.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.67.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.60.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.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/mysql': 2.15.27 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.66.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.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + '@types/pg': 8.15.6 + '@types/pg-pool': 2.0.7 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.38.3 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.33.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.207.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.212.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.214.0 + import-in-the-middle: 3.0.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/redis-common@0.38.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/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/semantic-conventions@1.40.0': {} + + '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1) + '@oxc-project/types@0.124.0': {} '@petamoriken/float16@3.9.3': {} + '@pinojs/redact@0.4.0': {} + '@playwright/test@1.59.1': dependencies: playwright: 1.59.1 + '@prisma/instrumentation@7.6.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + '@react-aria/focus@3.22.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@swc/helpers': 0.5.15 @@ -3658,6 +4313,65 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.7': {} + '@sentry/core@10.50.0': {} + + '@sentry/node-core@10.50.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': + dependencies: + '@sentry/core': 10.50.0 + '@sentry/opentelemetry': 10.50.0(@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/semantic-conventions@1.40.0) + import-in-the-middle: 3.0.1 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@sentry/node@10.50.0': + dependencies: + '@fastify/otel': 0.18.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-amqplib': 0.61.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-connect': 0.57.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dataloader': 0.31.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fs': 0.33.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-generic-pool': 0.57.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-graphql': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-hapi': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-ioredis': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-kafkajs': 0.23.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-knex': 0.58.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-koa': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-lru-memoizer': 0.58.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongodb': 0.67.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongoose': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql2': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pg': 0.66.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-redis': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-tedious': 0.33.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@prisma/instrumentation': 7.6.0(@opentelemetry/api@1.9.1) + '@sentry/core': 10.50.0 + '@sentry/node-core': 10.50.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.0(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + '@sentry/opentelemetry': 10.50.0(@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/semantic-conventions@1.40.0) + import-in-the-middle: 3.0.1 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + + '@sentry/opentelemetry@10.50.0(@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/semantic-conventions@1.40.0)': + 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/semantic-conventions': 1.40.0 + '@sentry/core': 10.50.0 + '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.15': @@ -3824,10 +4538,24 @@ snapshots: '@types/http-errors@2.0.5': {} + '@types/mysql@2.15.27': + dependencies: + '@types/node': 22.19.17 + '@types/node@22.19.17': dependencies: undici-types: 6.21.0 + '@types/pg-pool@2.0.7': + dependencies: + '@types/pg': 8.15.6 + + '@types/pg@8.15.6': + dependencies: + '@types/node': 22.19.17 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + '@types/qs@6.15.0': {} '@types/range-parser@1.2.7': {} @@ -3849,6 +4577,10 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 22.19.17 + '@types/tedious@4.0.14': + dependencies: + '@types/node': 22.19.17 + '@types/web-push@3.6.4': dependencies: '@types/node': 22.19.17 @@ -3870,7 +4602,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)) + vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)) '@vitest/expect@4.1.4': dependencies: @@ -3926,6 +4658,12 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + agent-base@7.1.4: {} ansi-regex@5.0.1: {} @@ -3968,6 +4706,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + atomic-sleep@1.0.0: {} + autoprefixer@10.5.0(postcss@8.5.9): dependencies: browserslist: 4.28.2 @@ -3977,6 +4717,8 @@ snapshots: postcss: 8.5.9 postcss-value-parser: 4.2.0 + balanced-match@4.0.4: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.19: {} @@ -4021,6 +4763,10 @@ snapshots: transitivePeerDependencies: - supports-color + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -4074,6 +4820,8 @@ snapshots: chownr@1.1.4: {} + cjs-module-lexer@2.2.0: {} + client-only@0.0.1: {} clsx@2.1.1: {} @@ -4215,9 +4963,11 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.38.4(@types/better-sqlite3@7.6.13)(@types/react@19.2.14)(better-sqlite3@11.10.0)(react@19.2.5): + drizzle-orm@0.38.4(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.15.6)(@types/react@19.2.14)(better-sqlite3@11.10.0)(react@19.2.5): optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/better-sqlite3': 7.6.13 + '@types/pg': 8.15.6 '@types/react': 19.2.14 better-sqlite3: 11.10.0 react: 19.2.5 @@ -4453,6 +5203,8 @@ snapshots: dependencies: fetch-blob: 3.2.0 + forwarded-parse@2.1.2: {} + forwarded@0.2.0: {} fraction.js@5.3.4: {} @@ -4480,6 +5232,8 @@ snapshots: transitivePeerDependencies: - supports-color + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4553,6 +5307,20 @@ snapshots: ieee754@1.2.1: {} + import-in-the-middle@2.0.6: + 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 + + 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 + indent-string@4.0.0: {} inherits@2.0.4: {} @@ -4751,10 +5519,16 @@ snapshots: minimalistic-assert@1.0.1: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + minimist@1.2.8: {} mkdirp-classic@0.5.3: {} + module-details-from-path@1.0.4: {} + ms@2.0.0: {} ms@2.1.3: {} @@ -4777,7 +5551,7 @@ snapshots: negotiator@0.6.3: {} - next@15.5.15(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + next@15.5.15(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@next/env': 15.5.15 '@swc/helpers': 0.5.15 @@ -4795,6 +5569,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.15 '@next/swc-win32-arm64-msvc': 15.5.15 '@next/swc-win32-x64-msvc': 15.5.15 + '@opentelemetry/api': 1.9.1 '@playwright/test': 1.59.1 sharp: 0.34.5 transitivePeerDependencies: @@ -4831,6 +5606,8 @@ snapshots: obug@2.1.1: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -4858,6 +5635,18 @@ snapshots: pathe@2.0.3: {} + pg-int8@1.0.1: {} + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -4866,6 +5655,33 @@ snapshots: pify@2.3.0: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-http@11.0.0: + dependencies: + get-caller-file: 2.0.5 + pino: 10.3.1 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pirates@4.0.7: {} playwright-core@1.59.1: {} @@ -4920,6 +5736,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -4941,6 +5767,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + process-warning@5.0.0: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -4965,6 +5793,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + random-bytes@1.0.0: {} range-parser@1.2.1: {} @@ -5061,6 +5891,8 @@ snapshots: dependencies: picomatch: 2.3.2 + real-require@0.2.0: {} + recharts-scale@0.4.5: dependencies: decimal.js-light: 2.5.1 @@ -5085,6 +5917,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-pkg-maps@1.0.0: {} resolve@1.22.12: @@ -5123,6 +5962,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -5234,6 +6075,10 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -5243,6 +6088,8 @@ snapshots: source-map@0.6.1: {} + split2@4.2.0: {} + stackback@0.0.2: {} statuses@2.0.2: {} @@ -5337,6 +6184,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -5474,7 +6325,7 @@ snapshots: jiti: 1.21.7 tsx: 4.21.0 - vitest@4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)): + vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0)) @@ -5497,13 +6348,14 @@ snapshots: vite: 8.0.8(@types/node@22.19.17)(esbuild@0.19.12)(jiti@1.21.7)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 22.19.17 '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) jsdom: 29.0.2 transitivePeerDependencies: - msw - vitest@4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)): + vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0)) @@ -5526,6 +6378,7 @@ snapshots: vite: 8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@1.21.7)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 22.19.17 '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) jsdom: 29.0.2 @@ -5575,4 +6428,6 @@ snapshots: xmlchars@2.2.0: {} + xtend@4.0.2: {} + zod@3.25.76: {} diff --git a/services/api/package.json b/services/api/package.json index 64f0415..146e9e7 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@oo/shared-types": "workspace:*", + "@sentry/node": "^10.50.0", "better-sqlite3": "^11.8.1", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -27,6 +28,8 @@ "nats": "^2.29.3", "node-fetch": "^3.3.2", "openid-client": "^6.3.4", + "pino": "^10.3.1", + "pino-http": "^11.0.0", "web-push": "^3.6.7", "zod": "^3.24.1" }, diff --git a/services/api/src/events/__tests__/nats.test.ts b/services/api/src/events/__tests__/nats.test.ts index 0b287b2..ee44204 100644 --- a/services/api/src/events/__tests__/nats.test.ts +++ b/services/api/src/events/__tests__/nats.test.ts @@ -121,13 +121,14 @@ describe('connectNats — bridge bus → JetStream', () => { it('swallows JetStream publish errors so the in-process bus keeps working', async () => { const { connectNats } = await import('../nats.js'); + const { logger } = await import('../../logger.js'); const { bus } = await import('../bus.js'); await connectNats('nats://test:4222'); // Force the next js.publish to reject. lastJsPublish.mockRejectedValueOnce(new Error('jetstream down')); - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const errSpy = vi.spyOn(logger, 'error'); expect(() => bus.publish('signals.task.synced', { userId: 'u', source: 'todoist', count: 0, syncedAt: '' }), @@ -142,12 +143,16 @@ describe('connectNats — bridge bus → JetStream', () => { describe('connectNats — failure mode', () => { it('logs a warning and stays silent when connect rejects', async () => { const { connectNats } = await import('../nats.js'); + const { logger } = await import('../../logger.js'); lastConnect.mockRejectedValueOnce(new Error('ECONNREFUSED')); - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, 'warn'); await expect(connectNats('nats://nope:4222')).resolves.toBeUndefined(); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('connection failed')); + expect(warnSpy).toHaveBeenCalledWith( + expect.objectContaining({ err: expect.anything() }), + expect.stringContaining('connection failed'), + ); }); }); diff --git a/services/api/src/events/nats.ts b/services/api/src/events/nats.ts index 2d4e61d..87b3a68 100644 --- a/services/api/src/events/nats.ts +++ b/services/api/src/events/nats.ts @@ -12,6 +12,7 @@ import type { NatsConnection, JetStreamClient, StreamConfig } from 'nats'; import { bus } from './bus.js'; +import { logger } from '../logger.js'; let nc: NatsConnection | null = null; let js: JetStreamClient | null = null; @@ -67,13 +68,13 @@ export async function connectNats(natsUrl: string): Promise { if (!js) return; const data = new TextEncoder().encode(JSON.stringify(payload)); js.publish(subject, data).catch((err: Error) => - console.error(`[nats] publish failed for ${subject}: ${err.message}`), + logger.error({ err, subject }, 'nats publish failed'), ); }); - console.log(`[nats] connected to ${natsUrl}, streams: ${STREAMS.map((s) => s.name).join(', ')}`); + logger.info({ url: natsUrl, streams: STREAMS.map((s) => s.name) }, 'nats connected'); } catch (err: any) { - console.warn(`[nats] connection failed — running without JetStream: ${err.message}`); + logger.warn({ err }, 'nats connection failed — running without JetStream'); } } diff --git a/services/api/src/index.ts b/services/api/src/index.ts index f68719c..28f1c92 100644 --- a/services/api/src/index.ts +++ b/services/api/src/index.ts @@ -1,7 +1,10 @@ import 'dotenv/config'; +import { logger } from './logger.js'; import express from 'express'; +import { pinoHttp } from 'pino-http'; import cookieParser from 'cookie-parser'; import cors from 'cors'; +import { tracingMiddleware } from './middleware/tracing.js'; import { config } from './config.js'; import { db, runMigrations } from './db/index.js'; import { tipScores, tipFeedback } from './db/schema.js'; @@ -26,13 +29,11 @@ import { registerProfileSubscriptions } from './profile/subscriber.js'; await mkdir(dirname(config.DATABASE_PATH), { recursive: true }); runMigrations(); -// Keep the API alive on stray async faults (e.g. a single bad admin route) -// rather than dropping the whole process. process.on('unhandledRejection', (reason) => { - console.error('[api] unhandledRejection', reason); + logger.error({ err: reason }, 'unhandledRejection'); }); process.on('uncaughtException', (err) => { - console.error('[api] uncaughtException', err); + logger.fatal({ err }, 'uncaughtException'); }); const app = express(); @@ -43,6 +44,15 @@ app.use( credentials: true, }), ); +app.use(tracingMiddleware); +app.use( + pinoHttp({ + logger, + genReqId: (req) => req.traceId, + customProps: (req) => ({ traceId: req.traceId }), + autoLogging: { ignore: (req) => req.url === '/health' }, + }), +); app.use(express.json()); app.use(cookieParser()); app.use(sessionMiddleware); @@ -56,16 +66,13 @@ app.use('/api/user', userRouter); app.use('/api/push', pushRouter); app.use('/api/admin', adminRouter); -// Proxy ml/serving endpoints through the API (admin-only). -// Allows admin UI to call /api/ml/stats/:userId, /api/ml/features/:userId -// without needing direct access to the ml/serving port. app.use('/api/ml', requireAuth as any, requireAdmin as any, async (req: Request, res: Response) => { const mlUrl = config.ML_SERVING_URL; const target = `${mlUrl}${req.path}`; try { const upstream = await fetch(target, { method: req.method, - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', traceparent: req.traceparent }, body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined, signal: AbortSignal.timeout(5000), }); @@ -82,7 +89,7 @@ async function purgeExpiredData() { await db.delete(tipScores).where(lt(tipScores.servedAt, cutoff)); await db.delete(tipFeedback).where(lt(tipFeedback.createdAt, cutoff)); } catch (err: any) { - console.error(`[purge] retention cleanup failed: ${err.message}`); + logger.error({ err }, 'retention cleanup failed'); } } @@ -90,7 +97,7 @@ purgeExpiredData(); setInterval(purgeExpiredData, 24 * 60 * 60 * 1000); app.listen(config.PORT, () => { - console.log(`oO API listening on http://localhost:${config.PORT}`); + logger.info({ port: config.PORT }, 'oO API listening'); }); if (config.NATS_URL) { diff --git a/services/api/src/logger.ts b/services/api/src/logger.ts new file mode 100644 index 0000000..72966a4 --- /dev/null +++ b/services/api/src/logger.ts @@ -0,0 +1,12 @@ +import pino from 'pino'; +import * as Sentry from '@sentry/node'; + +if (process.env['SENTRY_DSN']) { + Sentry.init({ + dsn: process.env['SENTRY_DSN'], + environment: process.env['NODE_ENV'] ?? 'development', + }); +} + +export const logger = pino({ level: process.env['LOG_LEVEL'] ?? 'info' }); +export { Sentry }; diff --git a/services/api/src/middleware/tracing.ts b/services/api/src/middleware/tracing.ts new file mode 100644 index 0000000..e393c80 --- /dev/null +++ b/services/api/src/middleware/tracing.ts @@ -0,0 +1,26 @@ +import { randomBytes } from 'crypto'; +import type { Request, Response, NextFunction } from 'express'; + +declare global { + namespace Express { + interface Request { + traceId: string; + traceparent: string; + } + } +} + +export function tracingMiddleware(req: Request, _res: Response, next: NextFunction): void { + const incoming = req.headers['traceparent'] as string | undefined; + let traceId: string; + if (incoming) { + const parts = incoming.split('-'); + traceId = parts.length === 4 && parts[1]?.length === 32 ? parts[1] : randomBytes(16).toString('hex'); + } else { + traceId = randomBytes(16).toString('hex'); + } + const parentId = randomBytes(8).toString('hex'); + req.traceId = traceId; + req.traceparent = `00-${traceId}-${parentId}-01`; + next(); +} diff --git a/services/api/src/routes/admin.ts b/services/api/src/routes/admin.ts index 1317e84..db407bf 100644 --- a/services/api/src/routes/admin.ts +++ b/services/api/src/routes/admin.ts @@ -1,4 +1,5 @@ import { type Router as ExpressRouter, Router, Response } from 'express'; +import { logger } from '../logger.js'; import { db, rawSqlite } from '../db/index.js'; import { users, @@ -766,7 +767,7 @@ router.post('/simulate/start', async (req: AuthenticatedRequest, res: Response) // — e.g. in the alpine api container) would emit an unhandled 'error' event // and crash the whole API process. child.on('error', async (err) => { - console.error('[sim] spawn error', err); + logger.error({ err }, 'sim: spawn error'); _simProcesses.delete(id); await db .update(simRuns) diff --git a/services/api/src/routes/auth.ts b/services/api/src/routes/auth.ts index 79778e9..0f378f6 100644 --- a/services/api/src/routes/auth.ts +++ b/services/api/src/routes/auth.ts @@ -5,6 +5,7 @@ import { db } from '../db/index.js'; import { users, sessions } from '../db/schema.js'; import { eq } from 'drizzle-orm'; import { config } from '../config.js'; +import { logger } from '../logger.js'; const router: ExpressRouter = Router(); @@ -36,7 +37,7 @@ router.get('/login', async (req: Request, res: Response) => { setTimeout(() => pendingStates.delete(state), 10 * 60 * 1000); const redirectUri = `${config.API_BASE_URL}/api/auth/callback`; - console.log('[auth] redirect_uri sent to Google:', redirectUri); + logger.info({ redirectUri }, 'auth: redirect_uri'); const authUrl = client.buildAuthorizationUrl(cfg, { redirect_uri: redirectUri, scope: 'openid email profile', @@ -72,7 +73,7 @@ router.get('/callback', async (req: Request, res: Response) => { expectedState: state, }); } catch (err) { - console.error('OAuth callback error', err); + logger.error({ err }, 'auth: OAuth callback error'); res.status(400).json({ error: 'OAuth error' }); return; } diff --git a/services/api/src/routes/recommender.ts b/services/api/src/routes/recommender.ts index 819b97c..a949c57 100644 --- a/services/api/src/routes/recommender.ts +++ b/services/api/src/routes/recommender.ts @@ -1,5 +1,6 @@ import { type Router as ExpressRouter, Router, Response } from 'express'; import { nanoid } from 'nanoid'; +import { logger } from '../logger.js'; import { db } from '../db/index.js'; import { integrationTokens, tipFeedback, tipViews, tipScores } from '../db/schema.js'; import { eq, and, desc } from 'drizzle-orm'; @@ -85,6 +86,7 @@ async function remotePolicy( userId: string, tasks: TipCandidate[], profile: Profile, + traceparent?: string, ): Promise<{ tipId: string; score: number; policy: string } | null> { const hour = new Date().getHours(); const dayOfWeek = new Date().getDay(); @@ -102,11 +104,10 @@ async function remotePolicy( profile_features: profile, }; - // Active policy: egreedy-v2 (promoted from shadow after offline sim — ADR-0012) try { const res = await fetch(`${config.ML_SERVING_URL}/score/egreedy/v2`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...(traceparent ? { traceparent } : {}) }, body: JSON.stringify(body), signal: AbortSignal.timeout(3000), }); @@ -146,6 +147,7 @@ async function fetchLlmCandidates( dayOfWeek: number, promptVersion: string | null, profile: Profile, + traceparent?: string, ): Promise { try { const tasks = signals.slice(0, 10).map((s) => ({ @@ -156,7 +158,7 @@ async function fetchLlmCandidates( })); const res = await fetch(`${config.ML_SERVING_URL}/generate`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...(traceparent ? { traceparent } : {}) }, body: JSON.stringify({ user_id: userId, context: { tasks, hour_of_day: hour, day_of_week: dayOfWeek }, @@ -226,6 +228,7 @@ router.post('/recommend', requireAuth, async (req: AuthenticatedRequest, res: Re dayOfWeek, requestedPromptVersion, profile, + req.traceparent, ); const allCandidates: TipCandidate[] = [...signalCandidates, ...llmResult.candidates]; @@ -240,7 +243,7 @@ router.post('/recommend', requireAuth, async (req: AuthenticatedRequest, res: Re const t0 = Date.now(); // Stage 2: score — egreedy bandit with random fallback - const scored = await remotePolicy(req.userId!, allCandidates, profile); + const scored = await remotePolicy(req.userId!, allCandidates, profile, req.traceparent); const latencyMs = Date.now() - t0; const tip = scored ? (allCandidates.find((t) => t.id === scored.tipId) ?? randomPolicy(allCandidates)) @@ -373,6 +376,7 @@ async function sendRewardWithRetry( reward: number, features: TipCandidate['features'], profile: Profile, + traceparent?: string, ): Promise { const body = JSON.stringify({ user_id: userId, @@ -387,7 +391,7 @@ async function sendRewardWithRetry( try { const res = await fetch(`${config.ML_SERVING_URL}/reward/egreedy/v2`, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...(traceparent ? { traceparent } : {}) }, body, signal: AbortSignal.timeout(3000), }); @@ -395,7 +399,7 @@ async function sendRewardWithRetry( throw new Error(`HTTP ${res.status}`); } catch (err: any) { if (attempt === 3) { - console.error(`[reward] failed after 3 attempts for tip ${tipId}: ${err.message}`); + logger.error({ tipId, err }, 'reward: failed after 3 attempts'); bus.publish('signals.tip.reward_failed', { userId, tipId, @@ -468,7 +472,7 @@ router.post('/tip/:id/feedback', requireAuth, async (req: AuthenticatedRequest, if (candidate) { // Re-fetch profile for the v2 ridge update; TTL cache makes this near-instant. const profile = await getProfile(req.userId!); - sendRewardWithRetry(req.userId!, tipId, reward, candidate.features, profile); + sendRewardWithRetry(req.userId!, tipId, reward, candidate.features, profile, req.traceparent); } // Delegate action to the owning signal source (e.g. mark done in Todoist) diff --git a/services/api/src/signals/__tests__/scheduler.test.ts b/services/api/src/signals/__tests__/scheduler.test.ts index 627a280..3e198bb 100644 --- a/services/api/src/signals/__tests__/scheduler.test.ts +++ b/services/api/src/signals/__tests__/scheduler.test.ts @@ -8,6 +8,11 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +vi.mock('../../logger.js', () => ({ + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn() }, +})); +import { logger } from '../../logger.js'; + // ── mock the drizzle query chain: db.select(...).from(...).where(...) ──────── let users: { userId: string }[] = []; const whereMock = vi.fn(async () => users); @@ -35,6 +40,7 @@ beforeEach(() => { whereMock.mockClear(); fromMock.mockClear(); selectMock.mockClear(); + vi.clearAllMocks(); vi.useFakeTimers(); }); @@ -102,8 +108,6 @@ describe('startTodoistSyncScheduler', () => { if (id === 'bad') throw new Error('todoist 401'); return []; }); - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); startTodoistSyncScheduler(60_000); await vi.advanceTimersByTimeAsync(10_001); @@ -112,19 +116,27 @@ describe('startTodoistSyncScheduler', () => { await Promise.resolve(); expect(fetchSignalsMock).toHaveBeenCalledTimes(3); - expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('sync error'), expect.anything()); - expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('2 ok, 1 failed')); + expect(logger.error).toHaveBeenCalledWith( + expect.objectContaining({ err: expect.anything() }), + 'scheduler: sync error', + ); + expect(logger.info).toHaveBeenCalledWith( + expect.objectContaining({ ok: 2, failed: 1 }), + 'scheduler: todoist sync', + ); }); it('survives a db query failure — logs and skips the tick', async () => { const { startTodoistSyncScheduler } = await import('../scheduler.js'); whereMock.mockRejectedValueOnce(new Error('sqlite locked')); - const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); startTodoistSyncScheduler(60_000); await vi.advanceTimersByTimeAsync(10_001); expect(fetchSignalsMock).not.toHaveBeenCalled(); - expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('failed to query users')); + expect(logger.error).toHaveBeenCalledWith( + expect.objectContaining({ err: expect.anything() }), + 'scheduler: failed to query users', + ); }); }); diff --git a/services/api/src/signals/aggregator.ts b/services/api/src/signals/aggregator.ts index e622d40..e37e0be 100644 --- a/services/api/src/signals/aggregator.ts +++ b/services/api/src/signals/aggregator.ts @@ -1,4 +1,5 @@ import type { Signal, SignalSource } from '@oo/shared-types'; +import { logger } from '../logger.js'; /** * Merges signals from all registered sources for a user. @@ -24,7 +25,7 @@ export class SignalAggregator { if (r.status === 'fulfilled') { signals.push(...r.value); } else { - console.error(`[aggregator] source '${this.sources[i].id}' failed:`, r.reason); + logger.error({ sourceId: this.sources[i]!.id, err: r.reason }, 'aggregator: source failed'); } } return signals; diff --git a/services/api/src/signals/scheduler.ts b/services/api/src/signals/scheduler.ts index 950865f..fcdbac1 100644 --- a/services/api/src/signals/scheduler.ts +++ b/services/api/src/signals/scheduler.ts @@ -13,6 +13,7 @@ import { db } from '../db/index.js'; import { integrationTokens } from '../db/schema.js'; import { eq } from 'drizzle-orm'; import { todoistSource } from './todoist.js'; +import { logger } from '../logger.js'; const DEFAULT_INTERVAL_MS = 15 * 60 * 1000; @@ -25,7 +26,7 @@ export function startTodoistSyncScheduler(intervalMs = DEFAULT_INTERVAL_MS): Nod .from(integrationTokens) .where(eq(integrationTokens.tokenStatus, 'active')); } catch (err: any) { - console.error(`[scheduler] failed to query users: ${err.message}`); + logger.error({ err }, 'scheduler: failed to query users'); return; } @@ -39,10 +40,10 @@ export function startTodoistSyncScheduler(intervalMs = DEFAULT_INTERVAL_MS): Nod let failed = 0; for (const r of results) { if (r.status === 'fulfilled') ok++; - else { failed++; console.error(`[scheduler] sync error:`, r.reason); } + else { failed++; logger.error({ err: r.reason }, 'scheduler: sync error'); } } - console.log(`[scheduler] todoist sync: ${ok} ok, ${failed} failed (${users.length} users)`); + logger.info({ ok, failed, total: users.length }, 'scheduler: todoist sync'); } // Run once shortly after startup, then on interval diff --git a/services/api/src/signals/todoist.ts b/services/api/src/signals/todoist.ts index 1b76437..816e327 100644 --- a/services/api/src/signals/todoist.ts +++ b/services/api/src/signals/todoist.ts @@ -3,6 +3,7 @@ import { db } from '../db/index.js'; import { integrationTokens } from '../db/schema.js'; import { eq, and } from 'drizzle-orm'; import { bus } from '../events/bus.js'; +import { logger } from '../logger.js'; const CACHE_TTL_MS = 30_000; @@ -46,7 +47,7 @@ export class TodoistSignalSource implements SignalSource { if (!res.ok) { if (res.status === 401) { - console.error(`[todoist] token expired for user ${userId}`); + logger.warn({ userId }, 'todoist: token expired'); bus.publish('signals.integration.token_expired', { userId, provider: 'todoist',