npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@absaoss-cps/ngx-ui-watchtower

v0.3.0

Published

Reusable Angular telemetry layer: application logs, scenario health telemetry and business/UX events, with an optional AWS CloudWatch RUM sink.

Readme

ngx-ui-watchtower

Telemetry for Angular apps, in three parts:

  • Scenarios — did a user journey succeed, and how long did it take?
  • BI events — which features are used?
  • Logs — what exactly happened?

Events go to AWS CloudWatch RUM. Logs go to a backend you provide.

The library's names are prefixed Uwt, short for UI Watchtower — UwtLoggerService, provideUwtTelemetry, UWT_LOG_API_PROVIDER.

Install

npm install @absaoss-cps/ngx-ui-watchtower
npm install aws-rum-web   # only for the AWS RUM sink

Requires Angular 21 or 22 and RxJS 7.8.

Setup

import {
  UWT_LOG_API_PROVIDER,
  provideUwtTelemetry
} from '@absaoss-cps/ngx-ui-watchtower';
import {
  UWT_RUM_CREDENTIALS_PROVIDER,
  provideUwtTelemetryRumSink
} from '@absaoss-cps/ngx-ui-watchtower/rum';

providers: [
  provideUwtTelemetry({
    application: 'my-app',
    environment: 'production',
    version: '1.0.0'
  }),

  // Where events go
  provideUwtTelemetryRumSink(),
  { provide: UWT_RUM_CREDENTIALS_PROVIDER, useExisting: MyRumCredentials },

  // Where logs go
  { provide: UWT_LOG_API_PROVIDER, useExisting: MyLogBackend }
];

There are no default destinations — you always choose them, so an app can't look wired up while shipping nothing. For local development, ship nowhere:

providers: [
  provideUwtTelemetry({ application: 'my-app', environment: 'dev', version }),
  provideUwtTelemetrySink('noop'),
  { provide: UWT_LOG_API_PROVIDER, useClass: UwtNoopLogApiProvider }
];

Optional settings are added with withScenarios(), withLogging(), withBIEvents() and withRedaction():

provideUwtTelemetry(
  { application: 'my-app', environment: 'production', version: '1.0.0' },
  withLogging({ minLevel: 'warn' }),
  withScenarios({ defaultTimeoutMs: 60_000 })
);

AWS credentials

Fetch them from your backend:

@Injectable({ providedIn: 'root' })
export class MyRumCredentials implements UwtRumCredentialsProvider {
  async load(): Promise<UwtRumBootstrap | null> {
    const res = await fetch('/rum/init', { cache: 'no-store' });
    if (!res.ok) return null;

    const { enabled, config, credentials } = await res.json();
    return enabled ? { config, credentials } : null; // null = don't ship
  }
}

Log backend

@Injectable({ providedIn: 'root' })
export class MyLogBackend implements UwtLogApiProvider {
  send(record: UwtLogRecord): void {
    void fetch('/api/logs', {
      method: 'POST',
      body: JSON.stringify(record),
      keepalive: true
    }).catch(() => undefined);
  }

  query(filter: UwtLogQuery): Promise<UwtLogRecord[]> {
    const params = new URLSearchParams(filter as Record<string, string>);
    return fetch(`/api/logs?${params}`).then((r) => r.json());
  }
}

Batching, retries and authentication are up to your backend class. If it batches, add a flush() method — the library calls it when the page is hidden or closed.

Using a different destination

RUM is one destination; a realm always has exactly one. To send somewhere else, swap it — application code doesn't change:

providers: [
  provideUwtTelemetry({ application: 'my-app', environment, version }),

  // Where events go — replaces provideUwtTelemetryRumSink() and
  // UWT_RUM_CREDENTIALS_PROVIDER
  provideUwtTelemetryDestination(MyBackendSink),

  // Where logs go — unchanged
  { provide: UWT_LOG_API_PROVIDER, useExisting: MyLogBackend }
];

MyBackendSink extends UwtTelemetrySink; in its record(), uwtClassifyTelemetryEvent(eventType, payload) tells a scenario from a BI event. Providing two destinations fails at startup.

Turning telemetry off

To run telemetry in some environments only, keep the same providers and add one switch — a value, or a factory run at startup:

provideUwtTelemetry(identity, withTelemetryEnabled(environment.name === 'dev'));
provideUwtTelemetry(
  identity,
  withTelemetryEnabled(() => inject(APP_CONFIG).telemetry)
);

When off, nothing is sent and nothing runs: no credentials fetch, no RUM SDK download, no broadcast channel, no page listeners. Your code doesn't change — start() still returns a scenario (inert: it records nothing and never settles), lookups find nothing, settled$ never emits, loggers do nothing and query() resolves []. The switch is read once at startup. It doesn't shrink the bundle, but the RUM SDK is loaded on demand, so it's never downloaded while telemetry is off.

Declare your names

Scenario, step, event and logger names are typed, so a typo is a compile error instead of a broken dashboard. Declare them once:

// src/app/telemetry.schema.ts
declare module '@absaoss-cps/ngx-ui-watchtower' {
  interface UwtScenarioNames {
    'load-customers': true;
  }
  interface UwtScenarioSteps {
    fetch: true;
    render: true;
  }
  interface UwtBIEventNames {
    export_clicked: true;
  }
  interface UwtLoggerNames {
    checkout: true;
  }
}

export {};

Until you declare any, plain strings are accepted.

Scenarios

A scenario measures one user journey.

class CustomersComponent {
  private readonly scenarios = inject(UwtScenarioTelemetryService);

  async load() {
    const scenario = this.scenarios.start({ name: 'load-customers' });

    try {
      scenario.step('fetch');
      const rows = await this.api.fetchCustomers();

      scenario.step('render');
      this.rows.set(rows);

      scenario.complete();
    } catch (error) {
      scenario.fail({ error });
    }
  }
}

End every scenario with one of these:

| Method | Use when | | -------------- | --------------------------------------------------- | | complete() | It worked | | fail() | Something broke | | incomplete() | An expected dead end, e.g. a search with no results | | cancel() | It no longer matters, e.g. the user moved on |

A scenario that is never ended times out after 30 seconds, and one still running when the page closes is recorded as abandoned. Calling an end method twice is harmless — the first one wins.

For RxJS, traceScenario ends the scenario for you:

this.api.fetchCustomers().pipe(traceScenario(scenario)).subscribe();

BI events

class ExportButton {
  private readonly bi = inject(UwtBITelemetryService);

  onClick() {
    this.bi.track('export_clicked', { format: 'csv' });
  }
}

Logging

class CheckoutService {
  private readonly logger = inject(UwtLoggerService).getLogger('checkout');

  async submit(scenario: UwtScenario) {
    this.logger.log('Submitting order');

    try {
      await this.api.pay();
    } catch (error) {
      // correlationId ties this line to the scenario's journey
      this.logger.error('Payment failed', {
        error,
        correlationId: scenario.id
      });
    }
  }
}

Read log lines back through your backend, for example everything written during one journey:

class SupportService {
  private readonly logs = inject(UwtLoggerService);

  linesOf(scenario: UwtScenario) {
    return this.logs.query({ correlationId: scenario.id });
  }
}

Signed-in user

const sink = inject(UwtTelemetrySink);

sink.setUserId(user.id); // on sign-in — an opaque id, never an email
sink.setUserId(undefined); // on sign-out

Privacy

Metadata accepts only flat values (strings, numbers, booleans, null), so objects full of personal data can't slip in. On top of that, the library redacts values under keys like password or token, strips URL query strings, and caps sizes. To also catch emails, card numbers and similar by their shape:

withRedaction({ scanValuePatterns: ['email', 'creditCard'] });

Send opaque user ids, and keep personal data out of your URLs.

Micro-frontends

In a page composed of fragments, the shell sends everything and the fragments forward to it — so a user is one session, not one per fragment.

// shell
providers: [
  provideUwtTelemetry({ application: 'shell', environment, version }),
  provideUwtTelemetryRumSink(),
  { provide: UWT_LOG_API_PROVIDER, useExisting: MyLogBackend },
  provideUwtTelemetryBroadcastHost()
];

// each fragment
providers: [
  provideUwtTelemetry({ application: 'cart', environment, version }),
  provideUwtTelemetrySink('broadcast')
];

Fragment code doesn't change. Give each fragment its own application name.

Seeing what is sent

Run in DevTools, no reload needed:

localStorage.setItem('debugScenario', 'true');
localStorage.setItem('debugBI', 'true');
localStorage.setItem('debugLogger', 'true'); // or 'checkout,cart' for some loggers

Testing

TestBed.configureTestingModule({
  providers: [
    provideUwtTelemetry({
      application: 'test',
      environment: 'test',
      version: '0'
    }),
    provideUwtTelemetrySink('noop'),
    { provide: UWT_LOG_API_PROVIDER, useClass: UwtNoopLogApiProvider }
  ]
});

Good to know

  • Telemetry never breaks your app: library errors are caught, and reported to the console only in dev mode.
  • It is safe under server-side rendering.
  • A scenario sends one event when it ends, not one per step — AWS RUM allows 200 events per session by default.

License

Apache License 2.0.