Design for issue #98: upload source maps per release and show symbolicated stack traces in the error detail view.
| Layer | Today |
|---|---|
| Ingest | POST /ingest/error accepts optional stack string; SDK sends release but API previously dropped it |
| Storage | ErrorGroup.top_stack (first line), ErrorOccurrence.stack (full raw string) |
| Dashboard | StackTraceView renders plain text — no frame parsing or symbolication |
| Upload | POST/GET /api/project/source-maps (JSON upload, metadata list); plan quotas per project |
| Symbolication | Server-side on GET /api/errors/:id; dashboard raw/symbolicated toggle on error detail |
Events already store release; errors must do the same before maps can be keyed.
- CI or CLI uploads
.mapfiles keyed by(project, app, release, bundle path). - User opens an error occurrence in production; stack shows original file, line, and function when a map exists.
- Toggle raw vs symbolicated stack; empty state when release has errors but no maps.
flowchart LR
CI[CI / CLI upload] --> API[Source map API]
SDK[SDK trackError] --> Ingest[POST /ingest/error]
Ingest --> DB[(Postgres + optional blob store)]
API --> DB
Detail[GET /api/errors/:id] --> Symbolicate[Symbolication service]
Symbolicate --> DB
Detail --> UI[Error detail StackTraceView]
Proposed SourceMapArtifact table:
| Column | Purpose |
|---|---|
project_id |
Tenant scope |
app |
SDK app label |
release |
Version string (matches error ingest) |
bundle_url |
Minified file URL/path the map applies to |
content or storage_key |
Map JSON (self-host bytea) or object-store key |
sha256 |
Dedupe / integrity |
uploaded_at |
Audit |
Retention: align with plan retentionDays; the nightly retention job deletes artifacts where uploaded_at is older than the project cutoff (same as events/errors).
- Parse V8 / Firefox / Safari stack line formats server-side.
- Resolve
(bundle, line, column)viasource-mapor@jridgewell/trace-mapping. - Key lookup:
(project_id, app, release)→ artifacts for bundle path. - Optional: cache symbolicated frames on
ErrorOccurrenceafter first read.
Fingerprinting: grouping stays on raw message + first stack line (minified). Symbolication is display-only unless we add a separate canonical fingerprint later.
| Phase | Scope | Status |
|---|---|---|
| 1 | Persist release on errors (ingest, schema, API, dashboard) |
Done |
| 2 | SourceMapArtifact schema + retention |
Done |
| 3 | Upload API (POST /api/project/source-maps) + CLI docs |
Done (JSON upload/list; CLI follow-up) |
| 4 | Symbolication engine + API field symbolicated_stack |
Done |
| 5 | Dashboard frame UI, settings/history page | Done |
| 6 | Quotas, tests, docs, README roadmap ✅ | Done |
- Add
releasetoErrorGroupandErrorOccurrence. - Accept
releaseinerrorSchema(SDK already sends it). - Update group
releaseon new occurrences (same pattern asenvironment). - Show release on error detail meta and per occurrence.
SourceMapArtifacttable: unique(project_id, app, release, bundle_url),content(TEXT), optionalstorage_key,sha256,size_bytes,uploaded_at.- Lookup helpers in
apps/api/src/lib/source-map-artifact.ts. - Retention sweep deletes stale maps per project plan
retentionDays(keeps maps when matching in-window errors exist).
Upload (EDITOR+ dashboard session or project API key for CI):
POST /api/project/source-maps
Content-Type: application/json
X-Project-Id: <project-uuid>
X-API-Key: tt_live_<publicId>_<secret>
{
"app": "web",
"release": "1.2.0",
"bundle_url": "https://cdn.example.com/assets/app.js",
"content": { "version": 3, "sources": ["..."], "mappings": "..." }
}CI callers may send Authorization: Bearer tt_live_... instead of X-API-Key. The key must belong to the project in X-Project-Id. Keys with an app restriction must upload maps for that same app label.
Returns 201 on create, 200 on replace (same key). Max size: 10 MB (MAX_SOURCE_MAP_BYTES). app and release are trimmed on upload and on all ingest routes so keys align with symbolication and retention.
List (any project member with read access):
GET /api/project/source-maps?app=web&release=1.2.0
X-Project-Id: <project-uuid>Returns metadata only (no map body). Implementation: apps/api/src/lib/source-map-upload.ts.
Future: multipart upload and CLI wrapper (npx @telemetry-tracker/cli upload-sourcemaps --release=1.0.0 ./dist/**/*.map).
For Vite (Vue, React, Svelte, etc.), add @telemetry-tracker/vite-plugin to upload maps automatically after vite build:
pnpm add -D @telemetry-tracker/vite-plugin// vite.config.ts
import { defineConfig } from "vite";
import { telemetrySourceMaps } from "@telemetry-tracker/vite-plugin";
export default defineConfig({
build: {
sourcemap: true,
},
plugins: [
telemetrySourceMaps({
apiKey: process.env.TT_API_KEY!,
projectId: process.env.TT_PROJECT_ID!,
release: process.env.TT_RELEASE!,
app: "my-web-app",
baseUrl: "https://example.com",
// baseApiUrl: "https://api.telemetry-tracker.com", // optional; self-host override
// deleteMapsAfterUpload: true, // remove .map from dist after upload
}),
],
});Create a project API key in Settings → API keys (same key as ingest). baseUrl must match the public URL where your minified JS is served — the plugin derives bundle_url from each .map path under build.outDir.
See sdk-vite.md for full options and a Vue + Vite example. Full option reference: sdk-vite.md.
Server-side stack parsing (V8, Firefox-style) and source map lookup by (project_id, app, release, bundle_url).
GET /api/errors/:id adds optional fields when maps exist:
symbolicated_top_stackon the error group — first symbolicated frame from the newest occurrence stack (nottop_stack, which stores the error message line)symbolicated_stackon each occurrence inoccurrences_list
Symbolication is display-only; grouping fingerprints stay on raw minified stacks. Implementation: apps/api/src/lib/stack-symbolicate.ts (@jridgewell/trace-mapping).
- Error detail (
/dashboard/errors/[id]) —StackTracePanelwith Raw / Symbolicated toggle per occurrence; empty-state hint links to source map settings when release has no maps. - Settings → Source maps (
/dashboard/settings/source-maps) — list uploaded artifacts by app + release (metadata only).
- Plan cap:
maxSourceMapArtifactsPerProject(FREE 25, PRO 250, BUSINESS 2 500). Re-uploading the same(app, release, bundle_url)replaces in place and does not consume an extra slot. - Enforced inside
upsertSourceMapArtifact(serializable transaction: count + create) onPOST /api/project/source-maps. - README and this doc updated; closes implementation scope for #98 pending v1.3.0 release promotion.
- Upload: dashboard session (EDITOR+) or project API key scoped to
X-Project-Id; rate limit per project. - Maps may contain source — treat as sensitive; same retention as telemetry.
- Reuses existing project API keys (same as ingest); per-key
allowed_appapplies to the uploadappfield.
- sdk-core.md —
trackError,releasein payloads - ARCHITECTURE.md — ingest pipeline
- ENTITLEMENTS.md — retention by plan tier
You can automatically upload source maps on every release using our GitHub Action:
name: Release Configuration
on:
release:
types: [published]
jobs:
upload-maps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Upload Source Maps
uses: ./.github/actions/upload-source-maps
with:
api_key: ${{ secrets.TT_API_KEY }}
project_id: "your-project-uuid-here"
release: ${{ github.event.release.tag_name }}
app: "my-telemetry-app"
artifact_path: "./dist"
base_url: "https://example.com"Create a project API key in Settings → API keys and store it as TT_API_KEY in your repository secrets.
The action defaults to the hosted cloud API (https://api.telemetry-tracker.com). For a self-hosted install, set base_api_url to your deployment’s public API URL — the same value as API_URL on the dashboard (no trailing slash):
- name: Upload Source Maps
uses: ./.github/actions/upload-source-maps
with:
api_key: ${{ secrets.TT_API_KEY }}
project_id: "your-project-uuid-here"
release: ${{ github.event.release.tag_name }}
app: "my-telemetry-app"
artifact_path: "./dist"
base_url: "https://cdn.example.com"
base_api_url: "https://telemetry-api.example.com"Use http://localhost:3001 (or your dev API port) when testing uploads against a local API from CI or a runner on your network.
See DEPLOYMENT.md for self-host setup and docs/hosted-cloud for the managed service.