Skip to main content

Overview

Loom’s crash tracking system captures errors and crashes from TypeScript (browser + Node.js) and Rust applications, groups them into issues via fingerprinting, tracks regressions across releases, and provides source context through uploaded source maps.
Crash tracking integrates with analytics for person identity and feature flags to capture active flags at crash time.

Key Features

  • Multi-platform crash capture: JavaScript, Node.js, Rust
  • Issue grouping via fingerprinting to deduplicate similar crashes
  • Issue lifecycle with unresolved/resolved/ignored/regressed states
  • Regression detection when resolved issues reappear
  • Source map uploads for TypeScript with source context
  • Rust backtrace symbolication with demangling

Core Concepts

Crash Event

A single crash occurrence:
pub struct CrashEvent {
    pub id: CrashEventId,
    pub org_id: OrgId,
    pub project_id: ProjectId,
    pub issue_id: Option<IssueId>,
    
    // Identity
    pub person_id: Option<PersonId>,
    pub distinct_id: String,
    
    // Error
    pub exception_type: String,
    pub exception_value: String,
    pub stacktrace: Stacktrace,
    
    // Context
    pub release: Option<String>,
    pub environment: String,
    pub platform: Platform,
    pub tags: HashMap<String, String>,
    pub active_flags: HashMap<String, String>,
    pub breadcrumbs: Vec<Breadcrumb>,
}

Issue

An aggregated group of similar crashes:
pub struct Issue {
    pub id: IssueId,
    pub short_id: String,           // "PROJ-123"
    pub fingerprint: String,        // SHA256 hash
    pub title: String,
    pub status: IssueStatus,
    pub level: IssueLevel,
    pub event_count: u64,
    pub user_count: u64,
    pub first_seen: DateTime<Utc>,
    pub last_seen: DateTime<Utc>,
}
Issue statuses:
StatusDescription
UnresolvedNew or ongoing issue
ResolvedFixed by developer
IgnoredIntentionally ignored
RegressedResolved issue reappeared

Platform Types

pub enum Platform {
    JavaScript,  // Browser
    Node,        // Node.js
    Rust,
}

Rust SDK

Install the SDK:
[dependencies]
loom-crash = { version = "0.1" }
loom-analytics = { version = "0.1" }  # Optional: for identity
loom-flags = { version = "0.1" }      # Optional: for flag capture

Setup with Integrations

use loom_crash::CrashClient;
use loom_analytics::AnalyticsClient;
use loom_flags::FlagsClient;

// Initialize crash client with integrations
let crash = CrashClient::builder()
    .api_key("loom_crash_xxx")
    .base_url("https://loom.example.com")
    .project("my-rust-app")
    .release(env!("CARGO_PKG_VERSION"))
    // Integrate with analytics for person identity
    .analytics(&analytics_client)
    // Integrate with flags to capture active flags
    .flags(&flags_client)
    .build()?;

// Install panic hook (captures all panics)
crash.install_panic_hook();

// Set user context
crash.set_user(UserContext {
    id: Some(user.id.to_string()),
    email: Some(user.email.clone()),
    ..Default::default()
});

// Add global tags
crash.set_tag("environment", "production");
crash.set_tag("server", hostname);

Capturing Errors

// Manual capture
if let Err(e) = do_something() {
    crash.capture_error(&e);
}

// Capture breadcrumb
crash.add_breadcrumb(Breadcrumb {
    category: "http".into(),
    message: Some("GET /api/users".into()),
    level: BreadcrumbLevel::Info,
    ..Default::default()
});

// Scoped context
crash.with_scope(|scope| {
    scope.set_tag("handler", "create_user");
    scope.set_extra("request_id", json!(request_id));
    process_request()?;
    Ok(())
})?;

// Shutdown (flushes pending events)
crash.shutdown().await?;

TypeScript SDK

Install the SDK:
npm install @loom/crash

Browser Setup

import { CrashClient } from '@loom/crash';
import { AnalyticsClient } from '@loom/analytics';
import { FlagsClient } from '@loom/flags';

const crash = new CrashClient({
  apiKey: 'loom_crash_xxx',
  baseUrl: 'https://loom.example.com',
  project: 'my-web-app',
  release: '1.2.3',
  environment: 'production',
  
  // Integrate with other SDKs
  analytics,
  flags,
  
  // Options
  maxBreadcrumbs: 100,
  beforeSend: (event) => {
    // Filter or modify events before sending
    if (event.exception_value.includes('ResizeObserver')) {
      return null; // Drop this event
    }
    return event;
  },
});

// Install global handlers
crash.installGlobalHandler();

// Set user context
crash.setUser({
  id: user.id,
  email: user.email,
});

Capturing Exceptions

// Manual capture
try {
  await riskyOperation();
} catch (error) {
  crash.captureException(error, {
    tags: { operation: 'risky' },
    extra: { attemptNumber: 3 },
  });
}

// Add breadcrumb
crash.addBreadcrumb({
  category: 'navigation',
  message: 'User navigated to /dashboard',
  level: 'info',
});

React Error Boundary

import { CrashBoundary } from '@loom/crash/react';

function App() {
  return (
    <CrashBoundary
      client={crash}
      fallback={<ErrorPage />}
      onError={(error, errorInfo) => {
        console.log('Crash captured:', error);
      }}
    >
      <MyApp />
    </CrashBoundary>
  );
}

Source Maps

Upload source maps to get readable stack traces for minified JavaScript:
1

Build your app with source maps

// vite.config.ts
export default {
  build: {
    sourcemap: true
  }
}
2

Upload source maps to Loom

loom crash upload-sourcemaps \
  --project my-app \
  --release 1.2.3 \
  --include ./dist/**/*.map \
  --include ./dist/**/*.js
3

Deploy your app

Source maps are matched by release version and file paths

Build Tool Integration

Vite plugin example:
// vite.config.ts
import { loomSourceMaps } from '@loom/crash-vite';

export default {
  plugins: [
    loomSourceMaps({
      project: 'my-app',
      release: process.env.GIT_SHA,
      apiKey: process.env.LOOM_CRASH_API_KEY,
    }),
  ],
};

Issue Fingerprinting

Crashes are grouped by fingerprint (SHA256 hash): Default algorithm:
  1. Exception type (most significant)
  2. Top 5 in-app stack frames (function + module)
  3. If no in-app frames, use all frames
Example:

Custom Fingerprinting

Override fingerprinting rules via API:
{
  "fingerprint_rules": [
    {
      "match_type": "exception_message",
      "pattern": "rate limit exceeded.*",
      "fingerprint": ["rate-limit-error"]
    },
    {
      "match_type": "module",
      "pattern": "third_party::*",
      "fingerprint": ["third-party-error", "{{ module }}"]
    }
  ]
}

Issue Lifecycle

1

Unresolved

New crash creates issue in Unresolved state
2

Resolve

Mark issue as Resolved when fixed:
curl -X POST https://loom.example.com/api/crash/projects/proj-123/issues/iss-456/resolve \
  -H "Authorization: Bearer <token>" \
  -d '{"resolved_in_release": "1.2.4"}'
3

Regression Detection

If new crash arrives for resolved issue:
  • Status changes to Regressed
  • times_regressed increments
  • Real-time alert sent via SSE
4

Ignore

Permanently ignore an issue:
curl -X POST https://loom.example.com/api/crash/projects/proj-123/issues/iss-456/ignore

API Endpoints

Capture Crashes

POST /api/crash/capture
endpoint
Ingest single crash event (requires Capture API key)Request:
{
  "distinct_id": "user_123",
  "exception_type": "TypeError",
  "exception_value": "Cannot read property 'x' of undefined",
  "stacktrace": {
    "frames": [
      {
        "function": "handleClick",
        "filename": "App.tsx",
        "lineno": 42,
        "colno": 15,
        "in_app": true
      }
    ]
  },
  "release": "1.2.3",
  "environment": "production",
  "platform": "javascript"
}

Manage Issues

GET /api/crash/projects/:id/issues
endpoint
List issues with filtersQuery Parameters:
  • status - Filter by status (unresolved, resolved, etc.)
  • sort - Sort by (last_seen, first_seen, event_count)
  • query - Search in title/message
GET /api/crash/projects/:id/issues/:issueId
endpoint
Get issue detail with recent eventsResponse:
{
  "issue": {
    "id": "iss_xxx",
    "short_id": "PROJ-123",
    "title": "TypeError: Cannot read property 'x' of undefined",
    "status": "unresolved",
    "event_count": 42,
    "user_count": 15,
    "first_seen": "2026-01-01T00:00:00Z",
    "last_seen": "2026-01-18T12:00:00Z"
  },
  "events": []
}
POST /api/crash/projects/:id/issues/:issueId/resolve
endpoint
Mark issue as resolvedRequest:
{
  "resolved_in_release": "1.2.4"
}

Upload Source Maps

POST /api/crash/projects/:id/artifacts
endpoint
Upload source maps (multipart/form-data)Form fields:
  • release - Release version
  • dist - Distribution variant (optional)
  • Files: .map and .js files

Real-time Updates (SSE)

Subscribe to crash events:
const eventSource = new EventSource(
  'https://loom.example.com/api/crash/projects/proj-123/stream',
  { headers: { Authorization: 'Bearer <token>' } }
);

eventSource.addEventListener('issue.regressed', (event) => {
  const data = JSON.parse(event.data);
  console.log(`Issue ${data.short_id} regressed!`);
  showNotification(`Issue regressed: ${data.title}`);
});
Events:
  • crash.new - New crash event
  • issue.new - New issue created
  • issue.regressed - Resolved issue regressed
  • issue.resolved - Issue resolved
  • issue.assigned - Issue assigned to user

Stack Frames

Stack frames include source context after symbolication:
{
  "function": "handleSubmit",
  "module": "components/Form",
  "filename": "Form.tsx",
  "lineno": 42,
  "colno": 15,
  "context_line": "    const result = await api.submit(data);",
  "pre_context": [
    "  async handleSubmit(data) {",
    "    try {",
    "      setLoading(true);"
  ],
  "post_context": [
    "      setSuccess(true);",
    "    } catch (error) {",
    "      setError(error);"
  ],
  "in_app": true
}

Best Practices

Upload Source Maps

Always upload source maps for production builds to get readable stack traces

Set Release Version

Use semantic versioning or commit SHA for release tracking

Add Breadcrumbs

Log user actions before crashes to aid debugging

Filter Noise

Use beforeSend hook to filter known browser quirks (ResizeObserver, etc.)

See Also

Analytics

Link crashes to person profiles

Sessions

Track crash-free session rates

Build docs developers (and LLMs) love