Dashboard Get started

Angular Instrumentation

One command wires up real browser tracing and logging with the OpenTelemetry Web SDK — fetch/XHR calls, page loads, and uncaught errors ship straight from the browser, no backend required.

Setup

bash
la init --dsn la_live_YOUR_TOKEN_HERE

Run this inside your Angular project. la init detects Angular from @angular/core in package.json, installs the OTel Web SDK packages, and writes src/la-tracing.ts.

Add one import as the very first line of main.ts, before bootstrapApplication:

src/main.ts
import './la-tracing';
import { bootstrapApplication } from '@angular/platform-browser';
// ...
Angular already loads zone.js, which intercepts thrown errors and routes them to Angular's own ErrorHandlerbefore they'd ever reach window.addEventListener('error', ...). A plain window listener alone will silently miss errors thrown inside Angular event handlers, template bindings, or change detection. Register the exported handler in app.config.ts so errors are actually captured:
src/app/app.config.ts
import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { LightarchErrorHandler } from '../la-tracing';

export const appConfig: ApplicationConfig = {
  providers: [
    // ...your existing providers
    { provide: ErrorHandler, useClass: LightarchErrorHandler },
  ],
};
Set your build target to ES2015 or ES2016. ZoneContextManager (used to keep async context correct across await/promise boundaries) breaks on ES2017+ due to a zone.js limitation.

What gets captured automatically

  • Fetch & XHR calls — every request your app makes: method, URL, status, duration
  • Page loads — navigation timing and every resource the page fetches (scripts, images, fonts)
  • Uncaught errors — via the ErrorHandler provider above, plus a window listener fallback for errors outside Angular's zone and unhandled promise rejections
console.log is not captured — there's no browser auto-instrumentation for it in OpenTelemetry. Use the exported logger to emit structured log records manually where you want them.

Screenshot needed

Traces page showing an Angular app's browser spans: GET fetch calls, resourceFetch entries for page assets, service=my-angular-app, durations in ms.

Emitting your own logs

la-tracing.ts exports a ready-to-use logger — inject it wherever you need it, for example in a component:

typescript
import { logger } from '../la-tracing';
import { SeverityNumber } from '@opentelemetry/api-logs';

logger.emit({
  severityNumber: SeverityNumber.INFO,
  severityText: 'INFO',
  body: 'checkout started',
  attributes: { 'order.id': orderId },
});

Tracing your own backend too

This only instruments what happens in the browser. To trace your API, database, and background jobs, run la init again inside your backend project — see the Node.js or other backend instrumentation guides. Traces from both sides link up automatically once you set propagateTraceHeaderCorsUrls (see below) to your backend's URL.

Verify it's working

bash
# Check spans are arriving (replace my-angular-app with your service name)
la query "SELECT service, severity, body, duration_ms FROM recent_spans WHERE service = 'my-angular-app' ORDER BY timestamp DESC LIMIT 10"
Connecting browser traces to your backend

By default, no trace headers are sent cross-origin — this is opt-in for privacy. To link a browser trace to the backend request it triggers, add your backend's URL to propagateTraceHeaderCorsUrls in la-tracing.ts:

typescript
new FetchInstrumentation({
  propagateTraceHeaderCorsUrls: [
    /^https:\/\/api\.yourapp\.com/,
  ],
})
Your backend's CORS config must also explicitly allow the traceparent and tracestate headers — they aren't in the browser's CORS-safelisted header set by default.
Reducing trace volume for high-traffic apps

100% of traces and logs are sent by default — the free-tier byte cap is the natural backstop. For high-traffic apps, sample down with TraceIdRatioBasedSampler:

typescript
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';

const tracerProvider = new WebTracerProvider({
  sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(0.1) }), // 10%
  // ...
});
Manual setup (if you'd rather not use the CLI)

Angular already ships zone.js, so use @opentelemetry/context-zone-peer-dep instead of @opentelemetry/context-zone — the latter bundles its own zone.js, which would double-patch global async APIs.

bash
npm install @opentelemetry/api @opentelemetry/api-logs \
  @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base @opentelemetry/sdk-logs \
  @opentelemetry/instrumentation @opentelemetry/instrumentation-fetch \
  @opentelemetry/instrumentation-xml-http-request @opentelemetry/instrumentation-document-load \
  @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-logs-otlp-http \
  @opentelemetry/resources @opentelemetry/semantic-conventions \
  @opentelemetry/context-zone-peer-dep
src/la-tracing.ts
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { logs, SeverityNumber } from '@opentelemetry/api-logs';
import { ErrorHandler } from '@angular/core';
import { ZoneContextManager } from '@opentelemetry/context-zone-peer-dep';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { resourceFromAttributes } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';

const SERVICE_NAME = 'my-angular-app';
const resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: SERVICE_NAME });

const tracerProvider = new WebTracerProvider({
  resource,
  spanProcessors: [
    new BatchSpanProcessor(
      new OTLPTraceExporter({
        url: 'https://la-ingest-gateway.lightarch.workers.dev/v1/traces',
        headers: { 'x-la-dsn': 'la_live_YOUR_TOKEN_HERE' },
      })
    ),
  ],
});

tracerProvider.register({ contextManager: new ZoneContextManager() });

registerInstrumentations({
  instrumentations: [
    new FetchInstrumentation({ propagateTraceHeaderCorsUrls: [] }),
    new XMLHttpRequestInstrumentation({ propagateTraceHeaderCorsUrls: [] }),
    new DocumentLoadInstrumentation(),
  ],
});

const loggerProvider = new LoggerProvider({
  resource,
  processors: [
    new BatchLogRecordProcessor({
      exporter: new OTLPLogExporter({
        url: 'https://la-ingest-gateway.lightarch.workers.dev/v1/logs',
        headers: { 'x-la-dsn': 'la_live_YOUR_TOKEN_HERE' },
      }),
    }),
  ],
});

logs.setGlobalLoggerProvider(loggerProvider);

export const logger = logs.getLogger('my-angular-app');

function emitErrorLog(message: string, err: { name?: string; stack?: string } | undefined, type: string) {
  logger.emit({
    severityNumber: SeverityNumber.ERROR,
    severityText: 'ERROR',
    body: message,
    attributes: { 'exception.type': err?.name ?? type, 'exception.stacktrace': err?.stack ?? '' },
  });
}

// Register this as a provider in app.config.ts — see above.
export class LightarchErrorHandler implements ErrorHandler {
  handleError(error: unknown): void {
    const err = error as { message?: string; name?: string; stack?: string } | undefined;
    emitErrorLog(err?.message ?? String(error), err, 'Error');
    console.error(error);
  }
}

// Fallback for errors outside Angular's zone, and unhandled promise rejections.
window.addEventListener('error', (event) => {
  emitErrorLog(event.message, event.error, 'Error');
});

window.addEventListener('unhandledrejection', (event) => {
  emitErrorLog(String(event.reason?.message ?? event.reason), event.reason, 'UnhandledRejection');
});