@browsonic/angular
v2.11.0
Published
Angular adapter for @browsonic/sdk — ErrorHandler, BrowsonicService, standalone provideBrowsonic() factory. Apache-2.0.
Maintainers
Readme
@browsonic/angular
Angular adapter for @browsonic/sdk — ErrorHandler drop-in, BrowsonicService injectable, standalone provideBrowsonic() factory, Router instrumentation, HttpClient companion.
Status: 0.3 surface — Angular 17+ standalone style. Module-NgModule apps work via the same providers (see Quickstart — NgModule). Pure TypeScript: no Angular / RxJS runtime dependency in this package's bundle (
@angular/core,@angular/router,@angular/common/http, andrxjsall stay peer-only type imports).
Install
npm install @browsonic/sdk @browsonic/angular@browsonic/sdk and @angular/core (≥17) are peer dependencies.
Quickstart — Angular 17+ standalone
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { ErrorHandler } from '@angular/core';
import { getBrowsonic } from '@browsonic/sdk';
import { provideBrowsonic, BrowsonicErrorHandler } from '@browsonic/angular';
import { AppComponent } from './app/app.component';
const sdk = getBrowsonic();
sdk.init({ apiEndpoint: 'https://your-ingest-endpoint.test/v1/events' });
bootstrapApplication(AppComponent, {
providers: [
...provideBrowsonic({ sdk }),
{ provide: ErrorHandler, useExisting: BrowsonicErrorHandler },
],
});Inject the service anywhere
import { Component, inject } from '@angular/core';
import { BrowsonicService } from '@browsonic/angular';
@Component({ selector: 'app-buy', template: '<button (click)="buy()">Buy</button>' })
export class BuyComponent {
private readonly browsonic = inject(BrowsonicService);
buy(): void {
try {
// …purchase logic
} catch (err) {
this.browsonic.captureError(err as Error);
}
}
}Quickstart — NgModule (Angular pre-standalone)
provideBrowsonic() is a standalone provider factory, but the values it returns are plain Provider[] — they work in AppModule.providers unchanged. Apps still on @NgModule({ ... }) bootstrap the same way:
// src/app/app.module.ts
import { NgModule, ErrorHandler } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { getBrowsonic } from '@browsonic/sdk';
import { provideBrowsonic, BrowsonicErrorHandler } from '@browsonic/angular';
import { AppComponent } from './app.component';
const sdk = getBrowsonic();
sdk.init({ apiEndpoint: 'https://your-ingest-endpoint.test/v1/events' });
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [
...provideBrowsonic({ sdk }),
{ provide: ErrorHandler, useExisting: BrowsonicErrorHandler },
],
bootstrap: [AppComponent],
})
export class AppModule {}Same provider tokens, same BrowsonicService injection sites. The only difference is where the providers array lives — bootstrapApplication for standalone, AppModule.providers for NgModule.
Router instrumentation
installRouterInstrumentation(router, options?) subscribes to Router.events, filters for NavigationEnd (via the urlAfterRedirects structural discriminator), and emits a category: 'navigation' breadcrumb on every successful route change. Returns an unsubscribe() callable for HMR-friendly teardown.
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { Router } from '@angular/router';
import { installRouterInstrumentation } from '@browsonic/angular';
@Component({ selector: 'app-root', template: '<router-outlet/>' })
export class AppComponent implements OnInit, OnDestroy {
private readonly router = inject(Router);
private off?: () => void;
ngOnInit(): void {
this.off = installRouterInstrumentation(this.router);
}
ngOnDestroy(): void {
this.off?.();
}
}The structural RouterLike shape only requires an events Observable, so test doubles work without @angular/router.
HttpClient companion
createBrowsonicHttpReporter(options?) returns a (request, error) => void callback that captures HttpClient failures to the SDK. Wire it into your own HttpInterceptor so the adapter stays peer-only on @angular/common/http and rxjs:
// src/app/browsonic-http.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { createBrowsonicHttpReporter } from '@browsonic/angular';
@Injectable()
export class BrowsonicHttpInterceptor implements HttpInterceptor {
private readonly report = createBrowsonicHttpReporter({
// Skip Browsonic's own ingest URL so a failed report can't trigger
// another report (would loop).
ignoreUrls: [/\/v1\/events$/],
// Suppress 401 / 404 — surfaced at the UI layer, not in the dashboard.
ignoreStatuses: [401, 404],
});
intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(req).pipe(
catchError((err: unknown) => {
this.report(req, err);
return throwError(() => err);
}),
);
}
}Register it via HTTP_INTERCEPTORS — standalone or NgModule, both shapes work:
// Standalone (app.config.ts)
import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from '@angular/common/http';
providers: [
provideHttpClient(withInterceptorsFromDi()),
{ provide: HTTP_INTERCEPTORS, useClass: BrowsonicHttpInterceptor, multi: true },
];
// NgModule (AppModule.providers)
import { HTTP_INTERCEPTORS } from '@angular/common/http';
providers: [{ provide: HTTP_INTERCEPTORS, useClass: BrowsonicHttpInterceptor, multi: true }];The reporter tags the active scope with angular.http.method + angular.http.status, attaches httpUrl (capped at 256 chars) and a truncated httpResponseBody (maxBodyLength: 0 disables body capture entirely). See http-interceptor.ts for all options.
API
BrowsonicErrorHandler
Drop-in for Angular's ErrorHandler provider. Implements the framework's duck-typed handleError(error: unknown): void shape. Calls sdk.captureError, then forwards to console.error so the Angular default dev-tools experience is preserved (consoleFallback: false to disable).
BrowsonicService
Injectable wrapper around the SDK. Methods: setUser, clearUser, captureError, captureMessage, addBreadcrumb, setTag, getSdk.
provideBrowsonic(options?)
Standalone-style provider factory. Returns the providers array that registers BrowsonicErrorHandler and BrowsonicService as singletons. Wire the framework's ErrorHandler token to useExisting: BrowsonicErrorHandler separately so consumers control whether the handler also relays to a custom upstream handler (we don't chain by default — Angular's ErrorHandler is single-binding).
installRouterInstrumentation(router, options?)
Subscribes to a RouterLike.events Observable and emits a category: 'navigation' breadcrumb on every NavigationEnd. Returns an unsubscribe() callable. Structural RouterLike / RouterEventLike types — no @angular/router runtime import. See Router instrumentation.
createBrowsonicHttpReporter(options?)
Returns a (request, error) => void callback that captures HttpClient failures. Filters by ignoreUrls (string or RegExp) and ignoreStatuses. Tags the scope with <ns>.method + <ns>.status, attaches httpUrl + truncated httpResponseBody. Designed to be wired into a consumer-owned HttpInterceptor so the adapter stays peer-only on @angular/common/http and rxjs. See HttpClient companion.
Defensive contract
- The host app must never crash because reporting failed.
- All SDK calls are wrapped in
try { … } catch {}. BrowsonicErrorHandler.handleErrornever throws — even when both the SDK andconsole.errorraise, the method returns silently.- All forwarders are no-ops when the SDK is unreachable.
Optional /decorated entry-point — @Injectable + signals
If you don't mind @angular/core running at runtime in this package's bundle (rather than the default type-only contract), you can import from @browsonic/angular/decorated to get:
BrowsonicDecoratedService—@Injectable({ providedIn: 'root' })version ofBrowsonicService. No more provider wiring; justinject(BrowsonicDecoratedService)from any component.provideBrowsonicUserSignal(options?)— provider factory that bridges aWritableSignal<UserContext | null>tosdk.setUser. Set the signal tonullto clear the user.
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideBrowsonicUserSignal } from '@browsonic/angular/decorated';
import { provideBrowsonic, BrowsonicErrorHandler } from '@browsonic/angular';
import { ErrorHandler } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
...provideBrowsonic({ sdk }),
{ provide: ErrorHandler, useExisting: BrowsonicErrorHandler },
...provideBrowsonicUserSignal({ initial: null }),
],
};
// any component
import { Component, inject } from '@angular/core';
import { BROWSONIC_USER_SIGNAL } from '@browsonic/angular/decorated';
@Component({ selector: 'app-login', template: '...' })
export class LoginComponent {
private readonly user = inject(BROWSONIC_USER_SIGNAL);
onLogin(profile: { id: string; email: string }) {
this.user.set(profile);
}
}The default @browsonic/angular entry stays peer-only on @angular/core. Pick /decorated only if you want the decorator + signal ergonomics.
What this package does NOT do
- NgZone-aware error capture. The default Angular zone catches errors; our
ErrorHandlerruns inside the zone. If your app usesNgZone.runOutsideAngular, errors there don't reachErrorHandler— callbrowsonic.captureError(err)from yourtry/catchblock manually. - Server-side rendering capture. Angular SSR runs in Node; the SDK is browser-only. Wire your own server-side telemetry.
License
Apache-2.0. See the repo root LICENSE and the package NOTICE.
