Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 8 additions & 40 deletions projects/ngx-ui-watchtower/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ fails where it always did: the logger needs a log provider but no
destination, scenarios need a destination and no log provider. The services
are root singletons, so each concern exists once per injector, and all of
them share the one destination and log provider. `settled$` is the core's
`onSettled` as an Observable that completes on destroy. It listens to the
core only while subscribed, so no record is copied for nobody; `traceScenario` is the
`onSettled` as an Observable that completes on destroy; `traceScenario` is the
core's `./rxjs` operator, reporting through this package's dev-mode
reporter. Framework-free applications compose the same three classes with
`uwtCreateTelemetry`.
Expand Down Expand Up @@ -211,10 +210,7 @@ providers: [
the same names. The dependency runs one way: the core imports neither
Angular nor this package, which a lint rule enforces. The library build
compiles against the core's built declarations, not its source, so it sees
the core exactly as an application does. An application upgrading from
0.4 changes nothing. Another framework's adapter needs only the core's
public entry points, as this package does;
`docs/ADAPTERS.md` is a short guide.
the core exactly as an application does.
- **What the core owns, this package re-exports.** The engine, the models
and name registries, the configuration types and defaults
(`uwtResolveTelemetryConfig` completes every `with*` feature), redaction,
Expand All @@ -233,29 +229,10 @@ providers: [
broadcast plumbing stay internal so they can change freely. Redaction is
the exception: a custom sink needs `uwtRedactMetadata`,
`uwtNormalizeError`, `uwtScrubString` and `uwtRedactConfigFor`.
- **The RUM sink is the core's too.** Its logic — the lazy `aws-rum-web`
import, startup retries, credential refresh, the pre-init buffer, the
settings check — lives in `@absaoss-cps/ui-watchtower/rum`. This package's
`/rum` keeps `UWT_RUM_CREDENTIALS_PROVIDER`, `provideUwtTelemetryRumSink()`
and `UwtRumTelemetrySink`, an injectable subclass of the core's.
- **The secondary entry keeps private copies of a few utilities.** ng-packagr
fixes each entry point's `rootDir` to its own `src`, so
`@absaoss-cps/ngx-ui-watchtower/rum` can't import the main entry's
internals by relative path and carries small copies of what it needs: its
Angular helpers and the reporter.
- **What is published is tested as published.** `npm run smoke` installs
both tarballs into consumers outside the workspace: a framework-free one
with neither Angular nor `aws-rum-web` installed, and an Angular
application, prerendered as a fragment without `aws-rum-web` — which then
holds no RUM code — and as a shell with `/rum`, whose RUM client is a chunk
loaded on demand.
- **Both packages share one version and release together.** Release Please
links their versions: a change to either releases both, and the same
release PR moves this package's dependency on the core to `^` the new
version. The release workflow publishes the core first and this package
only after it, so this package never depends on a core version npm doesn't
have. Publishing skips what npm already has, so running the workflow again
on the release tag completes a partial release.
internals by relative path and carries small copies of what it needs.
- **Test doubles live in the specs that use them**, declared inline, never in
a shared file and never exported. The one browser gap needing a stub is
`BroadcastChannel`, which jsdom doesn't implement.
Expand Down Expand Up @@ -1631,15 +1608,12 @@ convention, for an application that wants the pre-fallback behavior back.
## 11. Error handling and performance

- **Telemetry never breaks the application.** Every public entry point is
wrapped and never rethrows. The core hands a suppressed error (`'error'`),
or data it had to drop (`'warn'`), to the `UwtReporter` it is given. This
package gives it one that writes to the console, prefixed
wrapped and never rethrows. The core prints nothing by itself: it hands a
suppressed error, or data it had to drop, to the `UwtReporter` it is
given. This package gives it one that writes to the console, prefixed
`[ngx-ui-watchtower]`, in development mode only, so bugs surface in
development and production stays silent. What cannot work as configured
(`'config'`) — a second broadcast host, a host in a forwarding realm, RUM
settings of the wrong type — is printed in production too, as it always
was; without a reporter, the core prints it itself. Even a broken
`console.error` cannot turn a suppressed failure into a throw.
development and production stays silent. Even a broken `console.error`
cannot turn a suppressed failure into a throw.
- **Fail-open.** A broker outage, expired credentials or an SDK throw all
leave the application behaving exactly as with healthy telemetry. A failed
broker request at startup is retried with backoff for five minutes. A
Expand Down Expand Up @@ -2209,12 +2183,6 @@ providers: [
Code inside a fragment does not change: same services, same calls. One
session, one budget, one bundle.

| Realm | Destination | RUM and `UWT_RUM_CREDENTIALS_PROVIDER` | `UWT_LOG_API_PROVIDER` |
| -------------- | --------------------------------------------------- | -------------------------------------- | -------------------------------------- |
| Standalone app | Its own: `provideUwtTelemetryRumSink()`, or another | The app's | The app's |
| Shell | Its own, plus `provideUwtTelemetryBroadcastHost()` | The shell's: the only ones on the page | The shell's, also for fragments' logs |
| Fragment | `provideUwtTelemetrySink('broadcast')` | None: the shell's records for it | Bound by `'broadcast'`, to the shell's |

Either side may be built without Angular. The protocol — the messages, the
host, the forwarding sink and log provider — lives in
`@absaoss-cps/ui-watchtower`, and this package's classes are injectable
Expand Down
3 changes: 3 additions & 0 deletions projects/ngx-ui-watchtower/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ providers: [
```

Fragment code doesn't change. Give each fragment its own `application` name.
A shell or a fragment needn't be Angular: one built on
[`@absaoss-cps/ui-watchtower`](https://www.npmjs.com/package/@absaoss-cps/ui-watchtower)
connects the same way.

## Seeing what is sent

Expand Down
3 changes: 3 additions & 0 deletions projects/ngx-ui-watchtower/public-api.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"UwtBroadcastLogApiProvider": [
"class",
"new (): UwtBroadcastLogApiProvider",
"dispose(): void",
"flush(): void",
"ngOnDestroy(): void",
"query(filter: UwtLogQuery): Promise<UwtLogRecord[]>",
Expand All @@ -104,6 +105,7 @@
"UwtBroadcastTelemetrySink": [
"class",
"new (): UwtBroadcastTelemetrySink",
"dispose(): void",
"flush(beacon?: boolean | undefined): void",
"getSessionId(): string | undefined",
"getUserId(): string | undefined",
Expand Down Expand Up @@ -395,6 +397,7 @@
"UwtTelemetryBroadcastHost": [
"class",
"new (): UwtTelemetryBroadcastHost",
"dispose(): void",
"get received: number",
"ngOnDestroy(): void"
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
import { inject, Injectable, NgZone, OnDestroy } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { UwtBroadcastLogApiProvider as CoreBroadcastLogApiProvider } from '@absaoss-cps/ui-watchtower';
import { uwtBroadcastConnectionOptions } from '../../sinks/uwt-broadcast/uwt-broadcast.messages';
import {
UwtLogApiProvider,
UwtLogQuery,
UwtLogRecord,
uwtUuid
} from '@absaoss-cps/ui-watchtower';
import { uwtSafeVoid } from '../../utils/uwt-telemetry-safe.util/uwt-telemetry-safe.util';
import {
UwtBroadcastConnection,
uwtConnectBroadcastChannel,
uwtIsBroadcastLogRecord,
uwtIsBroadcastMessage
} from '../../sinks/uwt-broadcast/uwt-broadcast.messages';

/**
* How long a follower waits for the host to answer a log query before
* resolving to `[]`, in milliseconds.
*/
export const UWT_BROADCAST_LOG_QUERY_TIMEOUT_MS = 10_000;
uwtDevModeReporter,
uwtRunInsideZone,
uwtRunOutsideZone
} from '../../utils/uwt-telemetry-safe.util/uwt-telemetry-safe.util';

/** A log query waiting for the host's answer. */
interface UwtPendingLogQuery {
resolve: (records: UwtLogRecord[]) => void;
timer: ReturnType<typeof setTimeout>;
}
export { UWT_BROADCAST_LOG_QUERY_TIMEOUT_MS } from '@absaoss-cps/ui-watchtower';

/**
* Log API provider for a follower realm — the log counterpart of
Expand All @@ -45,82 +29,20 @@ interface UwtPendingLogQuery {
*/
@Injectable()
export class UwtBroadcastLogApiProvider
implements UwtLogApiProvider, OnDestroy
extends CoreBroadcastLogApiProvider
implements OnDestroy
{
private readonly connection: UwtBroadcastConnection =
uwtConnectBroadcastChannel('broadcastLogApi');

private readonly pending = new Map<string, UwtPendingLogQuery>();

/** Absent outside an application, e.g. a bare `Injector.create`. */
private readonly zone = inject(NgZone, { optional: true });

constructor() {
this.connection.onMessage((data) => this.onMessage(data));
}

/** @inheritdoc */
send(record: UwtLogRecord): void {
this.connection.post({ kind: 'log', record });
}

/** @inheritdoc */
query(filter: UwtLogQuery): Promise<UwtLogRecord[]> {
if (!this.connection.isOpen()) {
return Promise.resolve([]);
}

return new Promise((resolve) => {
const id = uwtUuid();
const timer = this.outsideZone(() =>
setTimeout(
() => this.answer(id, []),
UWT_BROADCAST_LOG_QUERY_TIMEOUT_MS
)
);
this.pending.set(id, { resolve, timer });
this.connection.post({ kind: 'log-query', id, filter });
super({
...uwtBroadcastConnectionOptions(),
runOutside: uwtRunOutsideZone(),
runInside: uwtRunInsideZone(),
report: uwtDevModeReporter
});
}

/** @inheritdoc */
flush(): void {
this.connection.post({ kind: 'log-flush' });
}

/** Resolves every unanswered query to `[]`, then closes the channel. */
ngOnDestroy(): void {
for (const id of [...this.pending.keys()]) {
this.answer(id, []);
}
this.connection.close();
}

private onMessage(data: unknown): void {
uwtSafeVoid('broadcastLogApi.receive', () => {
if (!uwtIsBroadcastMessage(data) || data.kind !== 'log-query-result') {
return;
}
// Other followers' answers arrive here too; answer() ignores them.
this.answer(data.id, data.records.filter(uwtIsBroadcastLogRecord));
});
}

private answer(id: string, records: UwtLogRecord[]): void {
const query = this.pending.get(id);
if (!query) {
return;
}
this.pending.delete(id);
clearTimeout(query.timer);
if (this.zone) {
this.zone.run(() => query.resolve(records));
} else {
query.resolve(records);
}
}

private outsideZone<T>(fn: () => T): T {
return this.zone ? this.zone.runOutsideAngular(fn) : fn();
this.dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
} from '@angular/core';
import { UwtTelemetryBroadcastHost } from '../../sinks/uwt-broadcast/uwt-broadcast-host.service';
import { UwtBroadcastTelemetrySink } from '../../sinks/uwt-broadcast/uwt-broadcast-telemetry.sink';
import { UWT_BROADCAST_CHANNEL } from '../../sinks/uwt-broadcast/uwt-broadcast.messages';
import {
UWT_BROADCAST_CHANNEL,
UwtBroadcastConnectionRef
} from '../../sinks/uwt-broadcast/uwt-broadcast.messages';
import { UwtNoopTelemetrySink } from '../../sinks/uwt-telemetry/uwt-noop-telemetry.sink/uwt-noop-telemetry.sink';
import {
UWT_DEFAULT_TELEMETRY_CONFIG,
Expand Down Expand Up @@ -376,6 +379,8 @@ export function provideUwtTelemetrySink(
switch (mode) {
case 'broadcast':
return makeEnvironmentProviders([
// One channel for the realm's sink and log provider.
UwtBroadcastConnectionRef,
provideUwtTelemetryDestination(UwtBroadcastTelemetrySink),
UwtBroadcastLogApiProvider,
{
Expand Down
Loading
Loading