@browsonic/angular
v2.14.3
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: stable, published to npm — check
package.jsonor npm for the current version. 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',
appKey: 'web',
apiKey: 'pk_live_...', // publishable key — safe to ship in the browser
});
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',
appKey: 'web',
apiKey: 'pk_live_...', // publishable key — safe to ship in the browser
});
@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.
Atlas page views with route templates
When page-view tracking is on, each NavigationEnd also calls trackPageView(template, { name, screen }) with:
- the route template — the matched route tree's parameterized path (
/users/:id), which the backend prefers over URL normalization; - the screen name — the deepest matched route's
data['browsonicName'](explicit) ordata['title'](the common Angular convention), feeding the SDK ≥ 3.13 screen-name channel; - the screen context — the deepest matched route's
data['browsonicScreen']({ id, kind, zone }), feeding the SDK ≥ 3.17 Atlas screen-context channel.
The trackPageViews option defaults to the core's atlas: true umbrella on SDK ≥ 3.14 (feature-detected via isAtlasEnabled()): when the host initialized with atlas: true, template tracking turns on with no second flag and the core hands organic page views over on the first template — no double counting. On older cores the default stays false; setting trackPageViews: true explicitly keeps the legacy contract (pair it with manualPageViews: true in the SDK init there).
// Route declarations feed all three channels:
const routes: Routes = [
{
path: 'billing/invoices/:id',
component: InvoiceDetailComponent,
data: {
browsonicName: 'Invoice Detail',
browsonicScreen: { id: 'billing.invoice-detail', kind: 'detail', zone: 'billing' },
},
},
];The snapshot walkers behind this are exported for hosts that call trackPageView themselves — see routeTemplateFromSnapshot, routeNameFromSnapshot, and routeScreenFromSnapshot.
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. When page-view tracking is on (trackPageViews, defaulting to the core's atlas: true umbrella on SDK ≥ 3.14), each navigation also sends trackPageView with the route template, screen name, and screen context. Returns an unsubscribe() callable. Structural RouterLike / RouterEventLike types — no @angular/router runtime import. See Router instrumentation.
routeTemplateFromSnapshot(root)
Joins the matched route tree's routeConfig.path segments into the parameterized route template (/users/:id/orders) — the authoritative form the Atlas backend prefers over URL normalization. Walks the primary-outlet chain (depth-capped); empty-path levels (componentless / lazy wrappers) are skipped. Returns '' when nothing matched, so callers can omit the field and fall back to URL normalization.
routeNameFromSnapshot(root)
Returns the deepest matched route's data['browsonicName'] (explicit) or data['title'] (the common Angular convention) — a host-authored human name for the screen, feeding the SDK ≥ 3.13 screen-name channel. Returns '' when the route tree declares none.
routeScreenFromSnapshot(root)
Returns the deepest matched route's data['browsonicScreen'] declaration ({ id, kind, zone }, typed as RouteScreenContextLike) for the SDK ≥ 3.17 Atlas screen-context channel, or undefined when no level declares one. Deepest declaration wins wholesale (no per-field merging across levels). Only string fields are forwarded; the SDK normalizes and validates each field independently, so an invalid value never breaks the page view.
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.
