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
la init --dsn la_live_YOUR_TOKEN_HERERun 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:
import './la-tracing';
import { bootstrapApplication } from '@angular/platform-browser';
// ...ErrorHandler — before 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:import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { LightarchErrorHandler } from '../la-tracing';
export const appConfig: ApplicationConfig = {
providers: [
// ...your existing providers
{ provide: ErrorHandler, useClass: LightarchErrorHandler },
],
};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
ErrorHandlerprovider above, plus awindowlistener 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:
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
# 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:
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [
/^https:\/\/api\.yourapp\.com/,
],
})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:
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.
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-depimport { 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');
});