From 66e40757c4ba06f4698395dcf36b88a1d5f158a9 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Sat, 24 Oct 2026 11:24:49 +1100 Subject: [PATCH 1/5] feat(backend): trace requests and queries through cr-service Bumps FastAPI to 0.162, which traces each request once a tracer provider is installed. This takes metrics, tracing and the /livez and /readyz probes from cr-service. --- backend/pyproject.toml | 5 +- backend/src/ref_backend/builder.py | 22 ++++++-- backend/src/ref_backend/core/config.py | 2 + backend/src/ref_backend/metrics.py | 10 ---- backend/tests/test_api/test_metrics.py | 8 +-- backend/tests/test_builder.py | 2 + backend/tests/test_tracing.py | 30 +++++++++++ backend/uv.lock | 72 +++++++++++++++++++++++--- 8 files changed, 123 insertions(+), 28 deletions(-) delete mode 100644 backend/src/ref_backend/metrics.py create mode 100644 backend/tests/test_tracing.py diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 3354a659..fa59b366 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ version = "0.8.2" description = "Backend for the Climate Rapid Evaluation Framework" requires-python = ">=3.12" dependencies = [ - "fastapi[standard]<1.0.0,>=0.114.2", + "fastapi[standard]<1.0.0,>=0.162.0", "httpx>=0.27", "pydantic>2.0", "psycopg[binary]<4.0.0,>=3.1.13", @@ -14,7 +14,8 @@ dependencies = [ "loguru", "pyyaml>=6.0", "fastapi-sqlalchemy-monitor>=1.1.3", - "prometheus-fastapi-instrumentator>=7.0.0", + "cr-service[tracing]>=0.6.0", + "opentelemetry-instrumentation-sqlalchemy>=0.51b0", ] [dependency-groups] diff --git a/backend/src/ref_backend/builder.py b/backend/src/ref_backend/builder.py index 28dd723a..9b327e2c 100644 --- a/backend/src/ref_backend/builder.py +++ b/backend/src/ref_backend/builder.py @@ -1,6 +1,10 @@ from dataclasses import asdict +from importlib.metadata import version import sentry_sdk +from cr_service import metrics, tracing +from cr_service.health import router as health_router +from cr_service.info import ServiceInfo from fastapi import FastAPI from fastapi.routing import APIRoute from fastapi_sqlalchemy_monitor import AlchemyStatistics, SQLAlchemyMonitor @@ -19,7 +23,8 @@ from ref_backend.api.main import api_router from ref_backend.caching import CacheControlMiddleware from ref_backend.core.config import Settings -from ref_backend.metrics import instrument_app + +SERVICE = ServiceInfo(name="ref-backend", version=version("ref-backend")) description = """ API for querying the results from the Climate Rapid Evaluation Framework (Climate REF). @@ -36,7 +41,8 @@ def custom_generate_unique_id(route: APIRoute) -> str: - return f"{route.tags[0]}-{route.name}" + # The cr-service health and metrics routes carry no tag. + return f"{route.tags[0]}-{route.name}" if route.tags else route.name class SlowQueryMonitor(ConditionalAction): @@ -95,9 +101,14 @@ def build_app(settings: Settings, ref_config: Config, database: Database) -> Fas if settings.SENTRY_DSN and settings.ENVIRONMENT != "local": sentry_sdk.init( dsn=str(settings.SENTRY_DSN), + environment=settings.ENVIRONMENT, release=settings.GIT_COMMIT, - enable_tracing=True, + traces_sample_rate=settings.SENTRY_TRACES_SAMPLE_RATE, + send_default_pii=False, ) + # FastAPI traces each request itself once a tracer provider is installed. + tracing.install_provider(SERVICE) + tracing.instrument_sqlalchemy(database._engine) app = FastAPI( title=settings.PROJECT_NAME, @@ -141,8 +152,9 @@ def build_app(settings: Settings, ref_config: Config, database: Database) -> Fas results_max_age=settings.RESULTS_CACHE_MAX_AGE, ) - # Registered before the static mount, so the SPA fallback does not swallow /metrics. - instrument_app(app) + # Registered before the static mount, so the SPA fallback does not swallow the probes or /metrics. + app.include_router(health_router) + metrics.instrument_app(app) app.include_router(api_router, prefix=settings.API_V1_STR) diff --git a/backend/src/ref_backend/core/config.py b/backend/src/ref_backend/core/config.py index fffede1d..500c7f3d 100644 --- a/backend/src/ref_backend/core/config.py +++ b/backend/src/ref_backend/core/config.py @@ -5,6 +5,7 @@ from pydantic import ( AnyUrl, BeforeValidator, + Field, HttpUrl, computed_field, ) @@ -66,6 +67,7 @@ def all_cors_origins(self) -> list[str]: PROJECT_NAME: str = "Climate Rapid Evaluation Framework" SENTRY_DSN: HttpUrl | None = None + SENTRY_TRACES_SAMPLE_RATE: float = Field(default=1.0, ge=0.0, le=1.0) REF_CONFIGURATION: str = "data" REF_READ_ONLY_DATABASE: bool = False """ diff --git a/backend/src/ref_backend/metrics.py b/backend/src/ref_backend/metrics.py deleted file mode 100644 index cc163869..00000000 --- a/backend/src/ref_backend/metrics.py +++ /dev/null @@ -1,10 +0,0 @@ -import fastapi -from prometheus_fastapi_instrumentator import Instrumentator - - -def instrument_app(app: fastapi.FastAPI) -> None: - """Expose ``http_requests_total`` and ``http_request_duration_seconds`` at ``/metrics``.""" - # The unique id function in the builder needs a tag on every route. - Instrumentator(excluded_handlers=["^/metrics$", "^/api/v1/utils/health-check/$"]).instrument(app).expose( - app, include_in_schema=False, tags=["metrics"] - ) diff --git a/backend/tests/test_api/test_metrics.py b/backend/tests/test_api/test_metrics.py index bd7223f3..68e0dcb0 100644 --- a/backend/tests/test_api/test_metrics.py +++ b/backend/tests/test_api/test_metrics.py @@ -7,8 +7,10 @@ def test_metrics_records_instrumented_requests(client): assert any(line.startswith("http_request_duration_seconds_count{") for line in lines) -def test_metrics_excludes_health_check(client): - client.get("/api/v1/utils/health-check/") +def test_metrics_excludes_probes(client): + assert client.get("/livez").status_code == 200 + assert client.get("/readyz").status_code == 200 response = client.get("/metrics") - assert "health-check" not in response.text + assert "/livez" not in response.text + assert "/readyz" not in response.text assert 'handler="/metrics"' not in response.text diff --git a/backend/tests/test_builder.py b/backend/tests/test_builder.py index e8e3d6bf..aa632928 100644 --- a/backend/tests/test_builder.py +++ b/backend/tests/test_builder.py @@ -18,3 +18,5 @@ def test_sentry_release_is_the_git_commit(monkeypatch): builder.build_app(settings, config, deps._get_database_dependency(settings, config)) assert calls[0]["release"] == "e145da768b1725ce708150597a565df09c923dc3" + assert calls[0]["environment"] == "staging" + assert calls[0]["traces_sample_rate"] == 1.0 diff --git a/backend/tests/test_tracing.py b/backend/tests/test_tracing.py new file mode 100644 index 00000000..25e3c64f --- /dev/null +++ b/backend/tests/test_tracing.py @@ -0,0 +1,30 @@ +import pytest +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + +TRACE_ID = "4bf92f3577b34da6a3ce929d0e0e4736" + + +@pytest.fixture(scope="module") +def exporter(): + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + trace.set_tracer_provider(provider) + return exporter + + +def test_request_span_continues_the_browser_trace(exporter, client): + exporter.clear() + + response = client.get( + "/api/v1/utils/about", headers={"traceparent": f"00-{TRACE_ID}-00f067aa0ba902b7-01"} + ) + + assert response.status_code == 200 + spans = [span for span in exporter.get_finished_spans() if span.kind == trace.SpanKind.SERVER] + assert spans + assert trace.format_trace_id(spans[0].context.trace_id) == TRACE_ID + assert spans[0].attributes["http.route"] == "/api/v1/utils/about" diff --git a/backend/uv.lock b/backend/uv.lock index 304e7897..c3dac2f4 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -9,10 +9,10 @@ resolution-markers = [ "python_full_version == '3.14.*' and sys_platform == 'emscripten'", "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version < '3.13' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] @@ -966,6 +966,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/17/48032919343afa2906112d458aceda2043afdb24a2ee8eb4c60904d8ddf1/coverage-7.17.0-py3-none-any.whl", hash = "sha256:bac487dce0d2aff28ddf1a533a916f771416eee0e493960f611e49037b878e9d", size = 217383, upload-time = "2026-10-22T16:06:48.471Z" }, ] +[[package]] +name = "cr-service" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "prometheus-fastapi-instrumentator" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "sentry-sdk", extra = ["fastapi"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/fa/028045d0861ddfda0fcf39ca2c8c11747ff92d717de58a80db1d30cb0090/cr_service-0.6.0.tar.gz", hash = "sha256:70c1aaef5c668756dd70862373b22714cea7063ff122d126dedf155e3c931fcd", size = 178029, upload-time = "2026-10-12T07:29:10.861Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/cd/b4e3468daa84cb9a6d3da5a8de93929ba8b46846aedd8b69314b90a39ee9/cr_service-0.6.0-py3-none-any.whl", hash = "sha256:907517bfe435f28dd26aa8bde0498274c4822ff111005300f0bc86a2ee1454c3", size = 51344, upload-time = "2026-10-12T07:29:09.724Z" }, +] + +[package.optional-dependencies] +tracing = [ + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, +] + [[package]] name = "cryptography" version = "50.0.2" @@ -1125,7 +1148,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.161.1" +version = "0.162.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -1135,9 +1158,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/3d/2cc1a0dbc2dac6e157b4ef12bac063e1016cb7c6d7395f76665e439cf1aa/fastapi-0.161.1.tar.gz", hash = "sha256:4076b88434452e20289379068e976c4a04c65ffc8e6be31510106e150a422f0f", size = 527406, upload-time = "2026-10-22T22:22:07.757Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/eb/2cf8c80abf09b2800aef0a83b41066fbfda8ee5495b7a08b9d1af40f7046/fastapi-0.162.0.tar.gz", hash = "sha256:6a22c1e2e6e32e0633ee7e170f00009a29b8228b16feb219ee42f32fd8d88a8f", size = 527867, upload-time = "2026-10-23T18:47:33.264Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/2a/101012d82931d9de3855384e1667e49c0e07a0048e3868f9fec1697293a9/fastapi-0.161.1-py3-none-any.whl", hash = "sha256:a1363080cf9aeedecd99f4e1a23669d7e77542227f6536f4d7bbd59f07ecaa94", size = 149049, upload-time = "2026-10-22T22:22:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4f/3f2c25c9b43970b5e9a08beb5e04776247edea684ddc0930f4f02f0bb6cb/fastapi-0.162.0-py3-none-any.whl", hash = "sha256:3785963de305dd09912426cb81f2d230fce98ab3521cdfe97265c8d14983128c", size = 148912, upload-time = "2026-10-23T18:47:31.734Z" }, ] [package.optional-dependencies] @@ -2685,6 +2708,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/1f/517eaa0187ba106a9da97160ce2add3a371812681dc440930b267f714e42/opentelemetry_exporter_otlp_proto_http-1.45.1-py3-none-any.whl", hash = "sha256:24a97cf3753c7fb52fad44a696e452ff371686339e2acf3309e2eda3d0230700", size = 22180, upload-time = "2026-10-06T17:32:43.946Z" }, ] +[[package]] +name = "opentelemetry-instrumentation" +version = "0.66b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/03/89e47ff8d52a4f83b343e6eb9ef1698ff45357216e5b6b2b21e0da5c5c7d/opentelemetry_instrumentation-0.66b1.tar.gz", hash = "sha256:e79a510f7d87c72d95e964ddb42193a0d9a75668c027d980eab032ea1322a5ce", size = 43308, upload-time = "2026-10-06T17:36:10.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/b2/d1413681ff43e13ac9860df27e1226d3199ab0b97b352ceea41abcc660a5/opentelemetry_instrumentation-0.66b1-py3-none-any.whl", hash = "sha256:4c4aa14dc9a24a02325a9d4c42c4d0208dbb1374c2b1b8fe6c9392d59f3e1008", size = 36904, upload-time = "2026-10-06T17:35:11.663Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-sqlalchemy" +version = "0.66b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/3e/d69fb08dacc4c248daedf7357732ddca7a0aaff7072a55e311d3da3ea51c/opentelemetry_instrumentation_sqlalchemy-0.66b1.tar.gz", hash = "sha256:a10043953fcba71911bf29a024f8cc337260c1ef0b4fc844b96cae0de0947baa", size = 18181, upload-time = "2026-10-06T17:36:38.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/05/f8cff0c68a7f9ab8fab01904be5a49c5214435109c2a6b6d173a974c10d9/opentelemetry_instrumentation_sqlalchemy-0.66b1-py3-none-any.whl", hash = "sha256:aa30b10d880d7e91cf94b23a92ac85cec09ffddd8f0d40256d7c510e3dd33971", size = 14425, upload-time = "2026-10-06T17:35:53.436Z" }, +] + [[package]] name = "opentelemetry-proto" version = "1.45.1" @@ -4063,11 +4117,12 @@ version = "0.8.2" source = { editable = "." } dependencies = [ { name = "climate-ref", extra = ["aft-providers", "postgres"] }, + { name = "cr-service", extra = ["tracing"] }, { name = "fastapi", extra = ["standard"] }, { name = "fastapi-sqlalchemy-monitor" }, { name = "httpx" }, { name = "loguru" }, - { name = "prometheus-fastapi-instrumentator" }, + { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "psycopg", extra = ["binary"] }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -4092,11 +4147,12 @@ dev = [ [package.metadata] requires-dist = [ { name = "climate-ref", extras = ["aft-providers", "postgres"], specifier = ">=0.19.0,<0.20" }, - { name = "fastapi", extras = ["standard"], specifier = ">=0.114.2,<1.0.0" }, + { name = "cr-service", extras = ["tracing"], specifier = ">=0.6.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.162.0,<1.0.0" }, { name = "fastapi-sqlalchemy-monitor", specifier = ">=1.1.3" }, { name = "httpx", specifier = ">=0.27" }, { name = "loguru" }, - { name = "prometheus-fastapi-instrumentator", specifier = ">=7.0.0" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.51b0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.1.13,<4.0.0" }, { name = "pydantic", specifier = ">2.0" }, { name = "pydantic-settings", specifier = ">=2.13.1,<3.0.0" }, From 4cf165b0fff0faed3a65599bbc8709540d0916e0 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Sat, 24 Oct 2026 11:24:51 +1100 Subject: [PATCH 2/5] feat(frontend): trace page loads and API calls with Sentry API calls carry traceparent, so backend spans join the browser trace. --- frontend/src/instrument.ts | 10 ++++++++++ frontend/src/main.tsx | 2 ++ 2 files changed, 12 insertions(+) diff --git a/frontend/src/instrument.ts b/frontend/src/instrument.ts index d54f49d8..47be580c 100644 --- a/frontend/src/instrument.ts +++ b/frontend/src/instrument.ts @@ -11,5 +11,15 @@ Sentry.init({ httpHeaders: false, httpBodies: [], }, + // Low traffic, so every page load is traced. + // The backend follows the browser's sampling decision, so this rate also sets how many API spans it records. + tracesSampleRate: 1.0, + // Same-origin calls plus the hosted APIs listed in `lib/apiEndpoint.ts`. + tracePropagationTargets: [ + /^\/api\//, + /^https:\/\/[\w.-]*climate-ref\.org\/api\//, + ], + // The backend's OpenTelemetry spans join the trace through `traceparent`. + propagateTraceparent: true, integrations: [], }); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 4ab80014..1578ffd5 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -66,6 +66,8 @@ const router = createRouter({ defaultPreloadStaleTime: 0, }); +Sentry.addIntegration(Sentry.tanstackRouterBrowserTracingIntegration(router)); + declare module "@tanstack/react-router" { interface Register { router: typeof router; From 4d4a7ddb490bdbabd47941ab2fc138b9e58d1f28 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Sat, 24 Oct 2026 11:25:18 +1100 Subject: [PATCH 3/5] docs: add changelog for #150 --- changelog/150.improvement.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/150.improvement.md diff --git a/changelog/150.improvement.md b/changelog/150.improvement.md new file mode 100644 index 00000000..095676f0 --- /dev/null +++ b/changelog/150.improvement.md @@ -0,0 +1 @@ +Added request tracing from the dashboard through the API, and `/livez` and `/readyz` health probes. From b92e43d2da643cef92d6769c20afa1c6b5529b07 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Sat, 24 Oct 2026 11:29:21 +1100 Subject: [PATCH 4/5] fix(backend): never cache the health probes --- backend/src/ref_backend/caching.py | 3 ++- backend/tests/test_caching.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/ref_backend/caching.py b/backend/src/ref_backend/caching.py index 29557827..a825d244 100644 --- a/backend/src/ref_backend/caching.py +++ b/backend/src/ref_backend/caching.py @@ -4,6 +4,7 @@ dashboard.climate-ref.org is fronted by Cloudflare, which handles the caching according to the headers we set. """ +from cr_service.health import PROBE_PATHS from starlette import status from starlette.datastructures import MutableHeaders from starlette.types import ASGIApp, Message, Receive, Scope, Send @@ -33,7 +34,7 @@ def __init__(self, app: ASGIApp, api_prefix: str, api_max_age: int, results_max_ self.app = app self.api_prefix = api_prefix.rstrip("/") + "/" self.results_prefix = self.api_prefix + "results/" - self.uncached_paths = frozenset({"/metrics", *(self.api_prefix + p for p in UNCACHED_API_PATHS)}) + self.uncached_paths = frozenset({*PROBE_PATHS, *(self.api_prefix + p for p in UNCACHED_API_PATHS)}) self.api_policy = f"public, max-age={api_max_age}" self.results_policy = f"public, max-age={results_max_age}" diff --git a/backend/tests/test_caching.py b/backend/tests/test_caching.py index 6808acba..1770d58b 100644 --- a/backend/tests/test_caching.py +++ b/backend/tests/test_caching.py @@ -99,7 +99,9 @@ def test_writes_are_not_stored(self, spa_client: TestClient): class TestLiveEndpointsAreNotStored: - @pytest.mark.parametrize("path", ["/api/v1/utils/health-check/", "/api/v1/utils/about", "/metrics"]) + @pytest.mark.parametrize( + "path", ["/api/v1/utils/health-check/", "/api/v1/utils/about", "/metrics", "/livez", "/readyz"] + ) def test_no_store(self, client: TestClient, path: str): r = client.get(path) assert r.status_code == 200 From ef5d74ca6498550da45a6a10439c24402f2d9d53 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Sun, 25 Oct 2026 08:04:29 +1100 Subject: [PATCH 5/5] feat(frontend): set the trace sample rate at build time VITE_SENTRY_TRACES_SAMPLE_RATE defaults to 0.1, because the dashboard serves around 500k API requests a month. --- Dockerfile | 2 ++ frontend/src/instrument.ts | 8 ++++++-- frontend/src/vite-env.d.ts | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 78761229..c6c7181c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,8 @@ RUN npm install COPY /frontend/ /frontend/ +# Unset keeps the default in `frontend/src/instrument.ts`. +ARG VITE_SENTRY_TRACES_SAMPLE_RATE RUN npm run build # Runtime container diff --git a/frontend/src/instrument.ts b/frontend/src/instrument.ts index 47be580c..a24b07ca 100644 --- a/frontend/src/instrument.ts +++ b/frontend/src/instrument.ts @@ -1,5 +1,7 @@ import * as Sentry from "@sentry/react"; +const DEFAULT_TRACES_SAMPLE_RATE = 0.1; + Sentry.init({ // This isn't real sensitive since it's a public DSN, but don't commit your real one! dsn: import.meta.env.PROD @@ -11,9 +13,11 @@ Sentry.init({ httpHeaders: false, httpBodies: [], }, - // Low traffic, so every page load is traced. // The backend follows the browser's sampling decision, so this rate also sets how many API spans it records. - tracesSampleRate: 1.0, + tracesSampleRate: Number( + import.meta.env.VITE_SENTRY_TRACES_SAMPLE_RATE || + DEFAULT_TRACES_SAMPLE_RATE, + ), // Same-origin calls plus the hosted APIs listed in `lib/apiEndpoint.ts`. tracePropagationTargets: [ /^\/api\//, diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 901fc32e..a1296e4a 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -3,3 +3,8 @@ /** Version of the frontend bundle, injected at build time from package.json. */ declare const __APP_VERSION__: string; + +interface ImportMetaEnv { + /** Share of page loads traced by Sentry, from 0 to 1, read at build time. */ + readonly VITE_SENTRY_TRACES_SAMPLE_RATE?: string; +}