audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add Logfire browser observability (#1224)

add browser-side tracing via @pydantic/logfire-browser so we can debug
issues like login redirect failures that are invisible from backend spans
alone. telemetry proxied through backend to keep write token server-side.

- bump logfire to >=4.26.0 for experimental forwarding proxy
- add POST /logfire-proxy/{path:path} endpoint in meta.py
- create frontend observability module (plyr-web service name)
- auto-instrument fetch, document-load, user-interaction, XHR
- propagate traceparent headers to API for distributed tracing
- fix pre-existing ambient.svelte.ts type error (Timeout vs number)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

authored by

nate nowack
Claude Opus 4.6
and committed by
GitHub
f62ba892 bf2e494b

+52 -7
+1 -1
backend/pyproject.toml
··· 20 20 "passlib[bcrypt]>=1.7.4", 21 21 "psycopg[binary]>=3.2.12", 22 22 "greenlet>=3.2.4", 23 - "logfire[fastapi,sqlalchemy]>=4.14.2", 23 + "logfire[fastapi,sqlalchemy]>=4.26.0", 24 24 "cachetools>=6.2.1", 25 25 "pytest-asyncio>=0.25.3", 26 26 "aioboto3>=15.5.0",
+13 -1
backend/src/backend/api/meta.py
··· 2 2 3 3 from typing import Annotated, Any 4 4 5 - from fastapi import APIRouter, Depends, HTTPException 5 + from fastapi import APIRouter, Depends, HTTPException, Request 6 6 from fastapi.responses import PlainTextResponse 7 + from logfire.experimental.forwarding import logfire_proxy 7 8 from sqlalchemy import select 8 9 from sqlalchemy.ext.asyncio import AsyncSession 9 10 ··· 147 148 ] 148 149 149 150 return {"tracks": tracks, "artists": artists, "albums": albums} 151 + 152 + 153 + @router.post("/logfire-proxy/{path:path}") 154 + async def proxy_browser_telemetry(request: Request): 155 + """forward browser telemetry to Logfire. 156 + 157 + proxies OpenTelemetry data from the browser SDK so the write token 158 + stays server-side. protected by CORS (allowed origins only) and 159 + global rate limiting. 160 + """ 161 + return await logfire_proxy(request)
+4 -4
backend/uv.lock
··· 388 388 { name = "fastapi", specifier = ">=0.115.0" }, 389 389 { name = "greenlet", specifier = ">=3.2.4" }, 390 390 { name = "httpx", specifier = ">=0.28.0" }, 391 - { name = "logfire", extras = ["fastapi", "sqlalchemy"], specifier = ">=4.14.2" }, 391 + { name = "logfire", extras = ["fastapi", "sqlalchemy"], specifier = ">=4.26.0" }, 392 392 { name = "mutagen", specifier = ">=1.47.0" }, 393 393 { name = "orjson", specifier = ">=3.11.4" }, 394 394 { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, ··· 1392 1392 1393 1393 [[package]] 1394 1394 name = "logfire" 1395 - version = "4.20.0" 1395 + version = "4.31.0" 1396 1396 source = { registry = "https://pypi.org/simple" } 1397 1397 dependencies = [ 1398 1398 { name = "executing" }, ··· 1403 1403 { name = "rich" }, 1404 1404 { name = "typing-extensions" }, 1405 1405 ] 1406 - sdist = { url = "https://files.pythonhosted.org/packages/58/77/ffe5d45e28db9907ab4ae5e475522a69bfc71fa1bef91d38bf7e5434b394/logfire-4.20.0.tar.gz", hash = "sha256:9f0253b4b72543e9fd6ad6bebd4ca319f50c4407876958e26fbf3065d3cc8499", size = 648954, upload-time = "2026-01-26T10:59:25.722Z" } 1406 + sdist = { url = "https://files.pythonhosted.org/packages/61/fc/21f923243d8c3ca2ebfa97de46970ced734e66ac634c1c35b6abb41300f1/logfire-4.31.0.tar.gz", hash = "sha256:361bfda17c9d70ada5d220211033bae06b871ddac9d5b06978bc0ceca6b8e658", size = 1080609, upload-time = "2026-03-27T19:00:46.339Z" } 1407 1407 wheels = [ 1408 - { url = "https://files.pythonhosted.org/packages/77/58/49d0a33b01ab85b96ece46775a879ea57429a468429af8565dccaec75981/logfire-4.20.0-py3-none-any.whl", hash = "sha256:ea492cace9b6e50399cc87b9d36abbc5f58af78839384c43a8c3d0fd14aeeffe", size = 240939, upload-time = "2026-01-26T10:59:22.565Z" }, 1408 + { url = "https://files.pythonhosted.org/packages/49/1a/8c860e35bf847ac0d647d94bad89dccbb66cbcafdd61d8334f8cc7cfdd58/logfire-4.31.0-py3-none-any.whl", hash = "sha256:49fad38b5e6f199a98e9c8814e860c8a42595bb81479b52a20413e53ee475b72", size = 308896, upload-time = "2026-03-27T19:00:43.107Z" }, 1409 1409 ] 1410 1410 1411 1411 [package.optional-dependencies]
frontend/bun.lockb

This is a binary file and will not be displayed.

+2
frontend/package.json
··· 34 34 }, 35 35 "dependencies": { 36 36 "@atproto/api": "^0.18.7", 37 + "@opentelemetry/auto-instrumentations-web": "^0.59.0", 38 + "@pydantic/logfire-browser": "^0.14.0", 37 39 "svelte-portal": "^2.2.1" 38 40 } 39 41 }
+1 -1
frontend/src/lib/ambient.svelte.ts
··· 220 220 error = $state<string | null>(null); 221 221 readonly temperatureUnit: TemperatureUnit = detectTemperatureUnit(); 222 222 223 - private fetchIntervalId: ReturnType<typeof window.setInterval> | null = null; 223 + private fetchIntervalId: number | null = null; 224 224 private lastFetchTime = 0; 225 225 private readonly STALE_MS = 30 * 60 * 1000; // 30 minutes 226 226 private baseValues: Map<string, string> = new Map();
+27
frontend/src/lib/observability.ts
··· 1 + import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web'; 2 + import logfire from '@pydantic/logfire-browser'; 3 + import { API_URL } from '$lib/config'; 4 + 5 + function deriveEnvironment(): string { 6 + if (API_URL.includes('localhost')) return 'local'; 7 + if (API_URL.includes('api-stg')) return 'staging'; 8 + return 'production'; 9 + } 10 + 11 + export function initObservability(): void { 12 + logfire.configure({ 13 + traceUrl: `${API_URL}/logfire-proxy/v1/traces`, 14 + serviceName: 'plyr-web', 15 + environment: deriveEnvironment(), 16 + instrumentations: [ 17 + getWebAutoInstrumentations({ 18 + '@opentelemetry/instrumentation-fetch': { 19 + propagateTraceHeaderCorsUrls: [new RegExp(API_URL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))] 20 + }, 21 + '@opentelemetry/instrumentation-xml-http-request': { 22 + propagateTraceHeaderCorsUrls: [new RegExp(API_URL.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))] 23 + } 24 + }) 25 + ] 26 + }); 27 + }
+4
frontend/src/routes/+layout.svelte
··· 25 25 import { jam } from '$lib/jam.svelte'; 26 26 import { search } from '$lib/search.svelte'; 27 27 import { browser } from '$app/environment'; 28 + import { initObservability } from '$lib/observability'; 28 29 let { children } = $props<{ children: any }>(); 29 30 let showQueue = $state(false); 30 31 ··· 57 58 // initialize auth and preferences once on mount (not on every navigation) 58 59 // this prevents repeated /auth/me calls for unauthenticated users 59 60 onMount(async () => { 61 + // set up browser observability before any fetch calls 62 + initObservability(); 63 + 60 64 // fetch sensitive images client-side (small payload, fast) 61 65 moderation.initialize(); 62 66