Browser Logging Without the Bloat

Browser Logging Without the Bloat

Browser Logging Without the Bloat

Most frontend logging setups involve installing an SDK, configuring a bundler plugin, and wiring up a provider at the root of your app. By the time you’re done, you’ve added 40KB to your bundle and you’re still not sure if it’s actually capturing the errors that matter.

Loguro takes a different approach. One <script> tag in your <head> and you get:

  • Console error/warning capture
  • Uncaught JS errors and unhandled promise rejections
  • Failed network requests (fetch + XHR)
  • Core Web Vitals (LCP, CLS, FID/INP)
  • Long task detection
  • Rage click detection
  • Page context on every log (URL, viewport, user agent)

No bundler. No npm install. No framework coupling.

Setup

Create a browser API key in your project — browser keys are safe to expose in client-side code because they’re locked to a list of allowed origins. The ingest service validates the Origin header on every request.

# Via command palette
--keys::create:browser:my-frontend

When prompted, add your allowed origins: https://myapp.com, http://localhost:3000.

Then drop the snippet in your <head>:

<script
  src="https://logu.ro/loguro.js"
  data-api-key="YOUR_BROWSER_KEY"
></script>

That’s it. Errors start flowing immediately.

What gets captured automatically

Console output

All console methods are intercepted by default and forwarded as structured logs. The original calls still fire — your browser DevTools are unaffected.

console.error('Payment failed', { userId: 42 }); // → level: error
console.warn('Slow query detected');              // → level: warning
console.info('User signed in');                   // → level: info
console.log('Cart updated');                      // → level: debug
console.debug('Cache miss', { key: 'user:42' }); // → level: debug

Uncaught exceptions

JS errors that bubble to window.onerror are captured with filename, line, and column:

{
  "level": "error",
  "message": "Cannot read properties of undefined (reading 'id')",
  "context": {
    "source": "window.onerror",
    "filename": "https://myapp.com/assets/checkout.js",
    "line": 142,
    "col": 18
  }
}

Unhandled promise rejections

fetch('/api/orders').then(res => res.json()); // forgot to .catch()
// → captured automatically as level: error

Failed network requests

Every fetch and XMLHttpRequest that returns 4xx/5xx or fails at the network level is logged with URL, method, status, and duration:

{
  "level": "error",
  "message": "Network request failed: POST /api/checkout",
  "context": {
    "source": "fetch",
    "url": "/api/checkout",
    "method": "POST",
    "status": 503,
    "duration_ms": 4821
  }
}

Page context (on every log)

Every log automatically includes where the user was and what they were running:

{
  "context": {
    "url": "https://myapp.com/checkout",
    "path": "/checkout",
    "referrer": "https://myapp.com/cart",
    "viewport": "1440x900",
    "user_agent": "Mozilla/5.0 ..."
  }
}

Core Web Vitals

LCP, CLS, and FID/INP are captured via PerformanceObserver and logged as info events — queryable in the console like any other log:

context.metric:"LCP" context.value_ms:>2500
context.metric:"CLS" context.value:>0.1

Long tasks

Any JS task blocking the main thread for more than 50ms is logged as a warning. These are the frames that cause jank:

{
  "level": "warning",
  "message": "Long task detected",
  "context": { "source": "performance", "duration_ms": 312, "start_time_ms": 1840 }
}

Rage clicks

Three or more clicks on the same element within 500ms signals something isn’t working. Captured as a warning with the element and path:

{
  "level": "warning",
  "message": "Rage click detected",
  "context": {
    "source": "rage-clicks",
    "element": "BUTTON",
    "element_id": "submit-payment",
    "clicks": 4,
    "path": "/checkout"
  }
}

Manual logging

window.Loguro is available globally for logging application events:

// After a successful action
Loguro.info('Checkout completed', { orderId: 'ord_9xk2', total: 99.99 });

// Custom error with context
Loguro.error('Payment declined', { gateway: 'stripe', code: 'card_declined', userId: 42 });

// Track slow operations
const start = Date.now();
await processOrder();
Loguro.info('Order processed', { duration_ms: Date.now() - start });

Controlling what gets captured

Two attributes give you full control:

data-intercept — console capture

<!-- Default: captures everything -->
<script src="https://logu.ro/loguro.js"
  data-api-key="..."
  data-intercept="all">
</script>

<!-- Only errors and warnings, skip info/debug -->
<script src="https://logu.ro/loguro.js"
  data-api-key="..."
  data-intercept="error,warning">
</script>

<!-- No console capture, manual Loguro.* calls only -->
<script src="https://logu.ro/loguro.js"
  data-api-key="..."
  data-intercept="none">
</script>
ValueCaptures
errorconsole.error, uncaught errors, unhandled rejections
warningconsole.warn
infoconsole.info
debugconsole.log, console.debug

data-capture — automatic features

<!-- Disable everything, manual only -->
<script src="https://logu.ro/loguro.js"
  data-api-key="..."
  data-intercept="none"
  data-capture="none">
</script>

<!-- Network monitoring and context only -->
<script src="https://logu.ro/loguro.js"
  data-api-key="..."
  data-capture="network,context">
</script>

<!-- Everything including breadcrumbs (click trail sent with errors) -->
<script src="https://logu.ro/loguro.js"
  data-api-key="..."
  data-capture="context,network,performance,vitals,rage-clicks,breadcrumbs">
</script>
FeatureDefaultWhat it does
contextURL, path, referrer, viewport, user agent on every log
networkFailed fetch/XHR with status and duration
performanceLong tasks + page load timing
vitalsLCP, CLS, FID/INP
rage-clicks3+ rapid clicks on the same element
breadcrumbsClick and network trail sent alongside errors

Resilience

The snippet is built to never interfere with your app:

  • Circuit breaker — stops sending after 5 consecutive failures, retries after 60s
  • Rate limiter — caps at 1,000 logs/minute client-side (ingest hard limit is 10k/min per key)
  • 5s timeout on every request via AbortController
  • Fire-and-forgetfetch is never awaited, nothing blocks the main thread

What’s next

  • Read the Getting Started guide for server-side logging and batch ingestion
  • See Query Syntax to filter by context.metric, context.path, context.status, and more