Skip to content
Merged
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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ How it is designed: [projects/ngx-ui-watchtower/DESIGN.md](projects/ngx-ui-watch
```text
projects/ui-watchtower/
src/ @absaoss-cps/ui-watchtower — the framework-independent engine
type-tests/ consumer type tests, without a framework
README.md usage guide, published to npm
package.json the published package's manifest
projects/ngx-ui-watchtower/
Expand Down Expand Up @@ -69,10 +70,11 @@ application does. Tests and the typecheck read the core's source.

Three checks guard what consumers depend on:

- **Type tests** (`projects/ngx-ui-watchtower/type-tests/`) compile, with the
Angular compiler, a consumer that uses every public export, and one that
augments the name registries: registered names accepted, others rejected,
`string` while nothing is registered.
- **Type tests** (`type-tests/` in each package) compile a consumer that uses
every public export, and one that augments the name registries: registered
names accepted, others rejected, `string` while nothing is registered. The
Angular package's are compiled by the Angular compiler, the core's by plain
`tsc`.
- **The API snapshots** (`public-api.snapshot.json` in each package) record
every export of the built packages with its members and signatures. After an intended change, run `npm run api:update` and review
the diff.
Expand Down
4 changes: 3 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ module.exports = {
collectCoverageFrom: [
'projects/ngx-ui-watchtower/**/*.ts',
'!projects/**/*.spec.ts',
'!projects/**/public-api.ts'
'!projects/**/public-api.ts',
// Compiled by the typecheck, never run.
'!projects/**/type-tests/**'
]
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"lint": "eslint \"**/*.ts\" --max-warnings=0",
"format": "prettier --write \"**/*.{ts,mjs,html}\"",
"format:check": "prettier --check \"**/*.{ts,mjs,html}\"",
"typecheck": "tsc -p projects/ui-watchtower/tsconfig.lib.json --noEmit && tsc -p projects/ui-watchtower/tsconfig.spec.json --noEmit && ng build --configuration=production --project=ui-watchtower && tsc -p projects/ngx-ui-watchtower/tsconfig.lib.json --noEmit && tsc -p projects/ngx-ui-watchtower/tsconfig.spec.json --noEmit && tsc -p projects/ngx-ui-watchtower/rum/tsconfig.lib.json --noEmit && tsc -p projects/ngx-ui-watchtower/rum/tsconfig.spec.json --noEmit && ngc -p projects/ngx-ui-watchtower/type-tests/tsconfig.json && ngc -p projects/ngx-ui-watchtower/type-tests/tsconfig.augmented.json",
"typecheck": "tsc -p projects/ui-watchtower/tsconfig.lib.json --noEmit && tsc -p projects/ui-watchtower/tsconfig.spec.json --noEmit && tsc -p projects/ui-watchtower/type-tests/tsconfig.json && tsc -p projects/ui-watchtower/type-tests/tsconfig.augmented.json && ng build --configuration=production --project=ui-watchtower && tsc -p projects/ngx-ui-watchtower/tsconfig.lib.json --noEmit && tsc -p projects/ngx-ui-watchtower/tsconfig.spec.json --noEmit && tsc -p projects/ngx-ui-watchtower/rum/tsconfig.lib.json --noEmit && tsc -p projects/ngx-ui-watchtower/rum/tsconfig.spec.json --noEmit && ngc -p projects/ngx-ui-watchtower/type-tests/tsconfig.json && ngc -p projects/ngx-ui-watchtower/type-tests/tsconfig.augmented.json",
"api:check": "npm run build && node scripts/api-snapshot.mjs",
"api:update": "npm run build && node scripts/api-snapshot.mjs --update"
},
Expand Down
36 changes: 27 additions & 9 deletions projects/ngx-ui-watchtower/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ providers: [
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.
- **What the core owns, this package re-exports.** The models and name
registries, the configuration types and defaults, redaction, `UwtScenario`
and the `UwtTelemetrySink` and `UwtLogApiProvider` contracts are the
core's. This package keeps the injection tokens, the services and
providers, and the destinations that need Angular. `UwtTelemetrySink` is a
plain abstract class in the core, not `@Injectable()`: Angular uses it as
the destination's token as before, and a custom sink still extends it.
- **Every export is explicit.** The barrels list their exports one by one —
in a published package each exported name is a compatibility promise. Id
generation, the clock, the fail-open wrappers, User Timings and the
Expand Down Expand Up @@ -271,6 +278,14 @@ scenario.step('fetch-data'); // ok
scenario.step('fetch-dat'); // error TS2345
```

The registries are declared once, in `@absaoss-cps/ui-watchtower`, and
re-exported here. TypeScript merges an augmentation into a re-exported
interface's declaration, so augmenting `@absaoss-cps/ngx-ui-watchtower`
checks the core's own APIs too: one vocabulary per application. A
framework-free application augments `@absaoss-cps/ui-watchtower` the same
way. The type tests in both packages' `type-tests/` hold each module path to
it.

A typo here would not just produce a wrong number. It would silently start a
**second, incomplete** metric series, and the alarm built on the first one
would keep reading healthy. Interpolating an id — ``step(`load-${id}`)`` —
Expand Down Expand Up @@ -1576,10 +1591,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. In development, a suppressed error is still
reported to `console.error`, so bugs surface; in production it stays
silent. Even a broken `console.error` cannot turn a suppressed failure into
a throw.
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. 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 All @@ -1594,15 +1611,16 @@ convention, for an application that wants the pre-fallback behavior back.
mark-cleanup fallback, a fragment's log-query timeout, the RUM client's
dispatch interval and the sink's retry and refresh timers — are scheduled
outside Angular's zone, so telemetry never keeps the application from
becoming stable.
becoming stable. The core takes that hook as `runOutside`; this package
passes `NgZone.runOutsideAngular`.
- **Bounded.** A 100-item buffer keeps events, page views and errors recorded
before the RUM client is ready, then replays them; it never grows past
that if initialization never completes.

The console report is itself wrapped in a try/catch — an application that has
patched or otherwise broken `console.error` cannot turn a suppressed
telemetry failure into a rethrow, in `uwtSafe`, or into a fresh unhandled
rejection, in `uwtSafeVoidMaybeAsync`'s async path. Tests
The report is itself wrapped in a try/catch — a reporter that throws, or an
application that has patched or otherwise broken `console.error`, cannot
turn a suppressed telemetry failure into a rethrow, in `uwtSafe`, or into a
fresh unhandled rejection, in `uwtSafeVoidMaybeAsync`'s async path. Tests
assert both halves, and assert that a sink or transport throwing on every
call leaves application code unaffected.

Expand Down
4 changes: 3 additions & 1 deletion projects/ngx-ui-watchtower/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,9 @@ declare module '@absaoss-cps/ngx-ui-watchtower' {
export {};
```

Until you declare any, plain strings are accepted.
Until you declare any, plain strings are accepted. The registries are
declared in `@absaoss-cps/ui-watchtower`, the engine this package builds on,
and re-exported here: augmenting this package, as above, covers both.

## Scenarios

Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,5 @@
import { InjectionToken } from '@angular/core';

/**
* Business and UX event tracking configuration.
*
* @group Interfaces
*/
export interface UwtBITelemetryConfig {
/** Milliseconds within which an identical event is treated as a double-fire. */
dedupWindowMs: number;

/**
* Distinct event keys tracked before expired ones are swept.
*
* Keyed per `eventName|scenarioId|feature|metadata`, which
* grows unbounded over a long session without this cap.
*/
dedupMaxKeys: number;

/**
* Whether redaction runs on BI events. On by default. Turning it off
* skips only the *configurable* PII scrubbing (`extraKeyPatterns`,
* value-pattern scanning, URL-query stripping) — the built-in credential
* denylist, size caps, error normalization and any
* `UwtRedactConfig.extraValueTransforms` still apply; see
* {@link uwtRedactConfigFor}.
*/
redact: boolean;
}

/** Default BI event tracking settings. */
export const UWT_DEFAULT_BI_TELEMETRY_CONFIG: UwtBITelemetryConfig = {
dedupWindowMs: 400,
dedupMaxKeys: 100,
redact: true
};
import type { UwtBITelemetryConfig } from '@absaoss-cps/ui-watchtower';

/**
* Resolved BI event configuration. Provided by {@link provideUwtTelemetry},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,5 @@
import { InjectionToken } from '@angular/core';
import {
UwtLoggerName,
UwtLogLevel
} from '../../models/uwt-log.models/uwt-log.models';

/**
* Application logging configuration.
*
* @group Interfaces
*/
export interface UwtLogConfig {
/** Records below this severity are discarded. */
minLevel: UwtLogLevel;

/**
* Per-logger overrides of {@link minLevel}, keyed by logger name. Loggers
* without an override use {@link minLevel}.
*
* ```typescript
* withLogging({ minLevel: 'warn', levels: { checkout: 'log' } })
* ```
*/
levels?: Partial<Record<UwtLoggerName, UwtLogLevel>>;

/**
* Also report `logger.error` calls to the telemetry sink as RUM errors. Off
* by default; competes for the same session event budget as scenario and
* BI data.
*
* Mirrors `detail.error` when supplied; a message-only call builds a
* synthetic `Error` from the redacted message text instead.
*/
mirrorErrorsToRum: boolean;

/**
* Whether redaction runs on log records. On by default. Turning it off
* skips only the *configurable* PII scrubbing (`extraKeyPatterns`,
* value-pattern scanning, URL-query stripping) — the built-in credential
* denylist, size caps, error normalization and any
* `UwtRedactConfig.extraValueTransforms` still apply; see
* {@link uwtRedactConfigFor}.
*/
redact: boolean;
}

/** Default application logging settings. */
export const UWT_DEFAULT_LOG_CONFIG: UwtLogConfig = {
minLevel: 'log',
mirrorErrorsToRum: false,
redact: true
};
import type { UwtLogConfig } from '@absaoss-cps/ui-watchtower';

/**
* Resolved logging configuration. Provided by {@link provideUwtTelemetry},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,77 +1,5 @@
import { InjectionToken } from '@angular/core';

/**
* Scenario telemetry configuration.
*
* @group Interfaces
*/
export interface UwtScenarioTelemetryConfig {
/**
* Milliseconds after which an unsettled scenario auto-settles as `timeout`.
* `0` disables the default; individual scenarios can still opt in.
*/
defaultTimeoutMs: number;

/**
* Emit an event per lifecycle transition instead of one packed event at
* settlement. Off by default.
*
* Counts against the RUM session's shared `sessionEventLimit` (200 by
* default). Suited to local debugging, not production traffic.
*
* Also governs per-step `debugScenario` console output, because the
* console prints one line per event actually sent and nothing else.
* With this off, a scenario prints a single line at settlement — the
* one record it sends.
*/
emitLifecycleEvents: boolean;

/**
* Maximum number of steps retained per scenario. Further steps are counted in
* `stepCount` but not stored, bounding the payload of a runaway loop.
*/
maxSteps: number;

/**
* Mirror scenario and step boundaries into `performance.mark` / `measure`,
* visible on the DevTools Performance track. Off by default.
*
* The `debugScenario` LocalStorage flag switches them on regardless of this
* setting. Marks are cleared when a scenario settles; see
* {@link markCleanupFallbackMs} for scenarios that never do.
*/
userTimings: boolean;

/**
* How long in milliseconds an unsettled scenario's User Timing marks can
* remain in the Performance buffer before a fallback clears them.
*
* Only applies when `userTimings` is enabled and `timeoutMs: 0`. `0`
* disables the fallback, so marks accumulate for the page's lifetime.
*/
markCleanupFallbackMs: number;

/**
* Whether redaction runs on scenario records. On by default. Turning it
* off skips only the *configurable* PII scrubbing (`extraKeyPatterns`,
* value-pattern scanning, URL-query stripping) — the built-in credential
* denylist, size caps, error normalization and any
* `UwtRedactConfig.extraValueTransforms` still apply; see
* {@link uwtRedactConfigFor}.
*/
redact: boolean;
}

/** Default scenario telemetry settings. */
export const UWT_DEFAULT_SCENARIO_TELEMETRY_CONFIG: UwtScenarioTelemetryConfig =
{
defaultTimeoutMs: 30_000,
emitLifecycleEvents: false,
maxSteps: 50,
userTimings: false,
markCleanupFallbackMs: 300_000,
redact: true
};
import type { UwtScenarioTelemetryConfig } from '@absaoss-cps/ui-watchtower';

/**
* Resolved scenario telemetry configuration. Provided by
Expand Down
Original file line number Diff line number Diff line change
@@ -1,67 +1,8 @@
import { inject, InjectionToken } from '@angular/core';
import {
UwtBITelemetryConfig,
UWT_DEFAULT_BI_TELEMETRY_CONFIG
} from '../uwt-bi-telemetry.config/uwt-bi-telemetry.config';
import {
UwtLogConfig,
UWT_DEFAULT_LOG_CONFIG
} from '../uwt-log.config/uwt-log.config';
import {
UwtScenarioTelemetryConfig,
UWT_DEFAULT_SCENARIO_TELEMETRY_CONFIG
} from '../uwt-scenario-telemetry.config/uwt-scenario-telemetry.config';
import { UWT_DEFAULT_EVENT_NAMESPACE } from '../../models/uwt-telemetry-common.models/uwt-telemetry-common.models';
import {
UWT_DEFAULT_REDACT_CONFIG,
UwtRedactConfig
} from '../../utils/uwt-telemetry-redact.util/uwt-telemetry-redact.util';

/**
* The identity every telemetry record and event carries — application name,
* deployment environment and version, plus the event-type namespace.
*
* @group Interfaces
*/
export interface UwtTelemetryIdentity {
/** Application name, e.g. `composition`. Becomes a session attribute. */
application: string;

/** Deployment environment, e.g. `dev`, `qa`, `prod`. */
environment: string;

/** Application version, e.g. `22.0.0`. */
version: string;

/**
* Prefix for the custom event types this library emits — `{namespace}.scenario`,
* `{namespace}.scenario.step` and `{namespace}.bi`.
*
* Defaults to `com.uwt`.
*/
eventNamespace?: string;
}

/**
* Library defaults for every concern not carried by {@link UwtTelemetryIdentity}
* — applied by {@link provideUwtTelemetry} to anything not overridden through a
* `with*()` feature.
*
* @group Utils
*/
export const UWT_DEFAULT_TELEMETRY_CONFIG: {
eventNamespace: string;
scenario: UwtScenarioTelemetryConfig;
logs: UwtLogConfig;
bi: UwtBITelemetryConfig;
redact: UwtRedactConfig;
} = {
eventNamespace: UWT_DEFAULT_EVENT_NAMESPACE,
scenario: UWT_DEFAULT_SCENARIO_TELEMETRY_CONFIG,
logs: UWT_DEFAULT_LOG_CONFIG,
bi: UWT_DEFAULT_BI_TELEMETRY_CONFIG,
redact: UWT_DEFAULT_REDACT_CONFIG
};
import type {
UwtRedactConfig,
UwtTelemetryIdentity
} from '@absaoss-cps/ui-watchtower';

/**
* The application's identity — shared by every concern; see
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NgZone } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { UwtLogRecord } from '../../models/uwt-log.models/uwt-log.models';
import { UwtLogRecord } from '@absaoss-cps/ui-watchtower';
import { UWT_BROADCAST_CHANNEL } from '../../sinks/uwt-broadcast/uwt-broadcast.messages';
import {
UWT_BROADCAST_LOG_QUERY_TIMEOUT_MS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { inject, Injectable, NgZone, OnDestroy } from '@angular/core';
import { UwtLogRecord } from '../../models/uwt-log.models/uwt-log.models';
import {
UwtLogApiProvider,
UwtLogQuery
} from '../uwt-log-api.provider/uwt-log-api.provider';
import {
uwtSafeVoid,
UwtLogQuery,
UwtLogRecord,
uwtUuid
} from '../../utils/uwt-telemetry-safe.util/uwt-telemetry-safe.util';
} from '@absaoss-cps/ui-watchtower';
import { uwtSafeVoid } from '../../utils/uwt-telemetry-safe.util/uwt-telemetry-safe.util';
import {
UwtBroadcastConnection,
uwtConnectBroadcastChannel,
Expand Down
Loading
Loading