From eebe70b4cbc0661a3575fc49a4f14ed8e0390011 Mon Sep 17 00:00:00 2001 From: fateeand Date: Fri, 16 Oct 2026 21:36:24 +0200 Subject: [PATCH 1/4] feat!: remove the BI event-type override --- projects/ngx-ui-watchtower/DESIGN.md | 24 ++++---------- .../uwt-bi-telemetry.config.ts | 2 +- .../lib/models/uwt-bi.models/uwt-bi.models.ts | 23 +++---------- .../uwt-bi-telemetry.service.spec.ts | 32 +++++-------------- .../uwt-bi-telemetry.service.ts | 9 ++---- .../uwt-telemetry-event.util.ts | 3 +- 6 files changed, 24 insertions(+), 69 deletions(-) diff --git a/projects/ngx-ui-watchtower/DESIGN.md b/projects/ngx-ui-watchtower/DESIGN.md index 6155eec..2072e56 100644 --- a/projects/ngx-ui-watchtower/DESIGN.md +++ b/projects/ngx-ui-watchtower/DESIGN.md @@ -115,7 +115,7 @@ Everything each entry point exports, by role. | Setup | `provideUwtTelemetry`, `withLogging`, `withScenarios`, `withBIEvents`, `withRedaction`, `UwtTelemetryFeature`, `provideUwtTelemetrySink`, `UwtTelemetryLocalSinkMode` (`'broadcast' \| 'noop'`), `provideUwtTelemetryBroadcastHost`, `provideUwtTelemetryDestination` + `UwtTelemetryDestinationOptions` | | Configuration | `UWT_TELEMETRY_IDENTITY` + `UwtTelemetryIdentity`; `UWT_LOG_CONFIG` + `UwtLogConfig`; `UWT_SCENARIO_TELEMETRY_CONFIG` + `UwtScenarioTelemetryConfig`; `UWT_BI_TELEMETRY_CONFIG` + `UwtBITelemetryConfig`; `UWT_REDACT_CONFIG`; `UWT_DEFAULT_TELEMETRY_CONFIG` (every default) | | Scenarios | `UwtScenarioTelemetryService`, `UwtScenario`, `traceScenario` + `UwtTraceScenarioOptions`; models `UwtScenarioOptions`, `UwtScenarioOutcome`, `UwtScenarioStepDetail`, `UwtScenarioRecord`, `UwtScenarioStep`, `UwtScenarioStepEvent`, `UwtScenarioAggregate`, `UwtScenarioStatus`, `UwtScenarioStepStatus` | -| BI events | `UwtBITelemetryService`; `UwtBIEvent`, `UwtBIEventDetail` (`eventType` override, `scenarioId`, `feature`) | +| BI events | `UwtBITelemetryService`; `UwtBIEvent`, `UwtBIEventDetail` (`scenarioId`, `feature`) | | Logging | `UwtLoggerService`, `UwtLogger`; `UwtLogDetail`, `UwtLogRecord`, `UwtLogLevel`, `UWT_LOG_LEVEL_ORDER`; `UWT_LOG_API_PROVIDER`, `UwtLogApiProvider`, `UwtLogQuery`; `UwtNoopLogApiProvider`, `UwtBroadcastLogApiProvider` | | Name registries | `UwtScenarioNames` / `UwtScenarioName`, `UwtScenarioSteps` / `UwtStepName`, `UwtBIEventNames` / `UwtBIEventName`, `UwtLoggerNames` / `UwtLoggerName` | | Sinks | `UwtTelemetrySink` (abstract), `UwtNoopTelemetrySink`, `UwtBroadcastTelemetrySink`, `UwtTelemetryBroadcastHost`, `UWT_BROADCAST_CHANNEL`, `UWT_DEFAULT_BROADCAST_CHANNEL` (`'ngx-ui-watchtower'`); `uwtClassifyTelemetryEvent` + `UwtTelemetrySinkEvent`, for a sink to tell what it received | @@ -167,7 +167,7 @@ the helper and the check. No destination at all still fails with `NG0201`. `uwtClassifyTelemetryEvent(eventType, payload)` tells `record` what it got, as a discriminated union — `scenario` (a `UwtScenarioRecord`), `scenario-step` (a `UwtScenarioStepEvent`), `bi` (a `UwtBIEvent`) or -`unknown` (an event-type override). It matches on the event type's ending, +`unknown` (any other event type). It matches on the event type's ending, so it works for any `eventNamespace` and for forwarded events; the broadcast host uses it too. @@ -475,20 +475,10 @@ and no `sessionId`/`userId`, which RUM attaches once per session. #### One event type -All BI events share a single event type, with `eventName` carried as a field -— one schema to query, one extended-metric definition. If an existing -dashboard is keyed on a specific legacy type, a single event can override it: - -```ts -biTelemetry.track( - 'click', - { source: 'toolbar' }, - { eventType: 'com.my-app.click' } -); -``` - -Use that only for migration — giving every event its own type is exactly what -the single-type design avoids. +All BI events share a single event type, `{ns}.bi`, with `eventName` carried +as a field — one schema to query, one extended-metric definition. There is no +per-event override: giving events their own types is exactly what the +single-type design avoids. ### Fields deliberately left out @@ -1624,7 +1614,7 @@ biTelemetry.track( Identical events within 400ms are collapsed into one, absorbing double-fires from a handler bound to both `click` and `keydown`. "Identical" means the same -name, scenario correlation, event type, feature and metadata content; +name, scenario correlation, feature and metadata content; differing in any one is a distinct event. ### Logging diff --git a/projects/ngx-ui-watchtower/src/lib/config/uwt-bi-telemetry.config/uwt-bi-telemetry.config.ts b/projects/ngx-ui-watchtower/src/lib/config/uwt-bi-telemetry.config/uwt-bi-telemetry.config.ts index b1b47c4..b8a2d3b 100644 --- a/projects/ngx-ui-watchtower/src/lib/config/uwt-bi-telemetry.config/uwt-bi-telemetry.config.ts +++ b/projects/ngx-ui-watchtower/src/lib/config/uwt-bi-telemetry.config/uwt-bi-telemetry.config.ts @@ -12,7 +12,7 @@ export interface UwtBITelemetryConfig { /** * Distinct event keys tracked before expired ones are swept. * - * Keyed per `eventName|scenarioId|eventType|feature|metadata`, which + * Keyed per `eventName|scenarioId|feature|metadata`, which * grows unbounded over a long session without this cap. */ dedupMaxKeys: number; diff --git a/projects/ngx-ui-watchtower/src/lib/models/uwt-bi.models/uwt-bi.models.ts b/projects/ngx-ui-watchtower/src/lib/models/uwt-bi.models/uwt-bi.models.ts index 5d8cce6..4819dce 100644 --- a/projects/ngx-ui-watchtower/src/lib/models/uwt-bi.models/uwt-bi.models.ts +++ b/projects/ngx-ui-watchtower/src/lib/models/uwt-bi.models/uwt-bi.models.ts @@ -40,13 +40,12 @@ export interface UwtBIEventNames {} export type UwtBIEventName = UwtRegistered; /** - * Correlation fields carried through unchanged onto the emitted - * {@link UwtBIEvent} — shared by {@link UwtBIEventDetail} and - * {@link UwtBIEvent} so neither declares its own copy. + * Optional correlation detail for a business/UX event, carried through + * unchanged onto the emitted {@link UwtBIEvent}. * * @group Interfaces */ -export interface UwtBIEventCorrelation { +export interface UwtBIEventDetail { /** Scenario this event happened inside. Pass {@link UwtScenario.id}. */ scenarioId?: string; @@ -54,20 +53,6 @@ export interface UwtBIEventCorrelation { feature?: string; } -/** - * Optional correlation detail for a business/UX event. - * - * @group Interfaces - */ -export interface UwtBIEventDetail extends UwtBIEventCorrelation { - /** - * Overrides the RUM event type for this one event. BI events normally - * share one type, with `eventName` as a field. Use this only when an - * existing dashboard or metric filter needs a specific event type. - */ - eventType?: string; -} - /** * A discrete business or UX event. * @@ -79,7 +64,7 @@ export interface UwtBIEventDetail extends UwtBIEventCorrelation { * @group Interfaces */ export interface UwtBIEvent - extends UwtBIEventCorrelation, Pick { + extends UwtBIEventDetail, Pick { /** * Event name, e.g. `export_clicked`. Declared by the application in * {@link UwtBIEventNames} — this library never hardcodes business event diff --git a/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.spec.ts b/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.spec.ts index be9b835..e8b2de5 100644 --- a/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.spec.ts +++ b/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.spec.ts @@ -329,13 +329,6 @@ describe('UwtBITelemetryService', () => { expect(sink.ofType(UWT_TELEMETRY_EVENT_TYPE.bi)).toHaveLength(2); }); - it('should not collapse the same event across different event types', () => { - service.track('export_clicked', undefined, { eventType: 'com.uwt.a' }); - service.track('export_clicked', undefined, { eventType: 'com.uwt.b' }); - - expect(sink.events).toHaveLength(2); - }); - it('should allow the event again once the window has passed', () => { const nowSpy = jest.spyOn(performance, 'now'); nowSpy.mockReturnValue(1_000_000); @@ -404,9 +397,9 @@ describe('UwtBITelemetryService', () => { expect(sink.ofType(UWT_TELEMETRY_EVENT_TYPE.bi)).toHaveLength(2); }); - it('should not collapse two events whose eventType/feature only collide under naive delimiter joining', () => { - service.track('checkout', undefined, { eventType: 'x|y', feature: 'z' }); - service.track('checkout', undefined, { eventType: 'x', feature: 'y|z' }); + it('should not collapse two events whose scenarioId/feature only collide under naive delimiter joining', () => { + service.track('checkout', undefined, { scenarioId: 'x|y', feature: 'z' }); + service.track('checkout', undefined, { scenarioId: 'x', feature: 'y|z' }); expect(sink.events).toHaveLength(2); }); @@ -664,22 +657,13 @@ describe('UwtBITelemetryService', () => { expect(namespacedSink.events[0].eventType).toBe('com.test-app.bi'); }); - it('should let one event override the type, for a legacy dashboard', () => { - service.track( - 'click', - { source: 'toolbar' }, - { - eventType: 'com.test-app.click' - } - ); - - expect(sink.events[0].eventType).toBe('com.test-app.click'); - expect(sink.events[0].payload.eventName).toBe('click'); - }); + it('should always use the namespace type, even when a caller still passes an eventType', () => { + service.track('click', undefined, { + eventType: 'com.test-app.click' + } as never); - it('should fall back to the configured type when the override is empty', () => { - service.track('click', undefined, { eventType: '' }); expect(sink.events[0].eventType).toBe('com.uwt.bi'); + expect(sink.events[0].payload.eventName).toBe('click'); }); }); diff --git a/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.ts b/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.ts index 4ea21f9..ac0f9e1 100644 --- a/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.ts +++ b/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.ts @@ -81,7 +81,7 @@ export class UwtBITelemetryService { * as declared in {@link UwtBIEventNames}. Treat it as a metric dimension: * keep the cardinality low and never interpolate an identifier into it. * @param metadata flat attributes describing the interaction - * @param detail optional scenario correlation, feature and event-type override + * @param detail optional scenario correlation and feature */ track( eventName: UwtBIEventName, @@ -119,13 +119,11 @@ export class UwtBITelemetryService { application: this.identity.application }; - const eventType = detail?.eventType || this.eventTypes.bi; - uwtDebugWrite('debugBI', () => - writeToConsole(eventName, eventType, event) + writeToConsole(eventName, this.eventTypes.bi, event) ); - this.sink.record(eventType, event); + this.sink.record(this.eventTypes.bi, event); }); } @@ -137,7 +135,6 @@ export class UwtBITelemetryService { const key = JSON.stringify([ eventName, detail?.scenarioId ?? '', - detail?.eventType ?? '', detail?.feature ?? '', this.metadataKey(metadata) ]); diff --git a/projects/ngx-ui-watchtower/src/lib/utils/uwt-telemetry-event.util/uwt-telemetry-event.util.ts b/projects/ngx-ui-watchtower/src/lib/utils/uwt-telemetry-event.util/uwt-telemetry-event.util.ts index b5881c3..aa04d35 100644 --- a/projects/ngx-ui-watchtower/src/lib/utils/uwt-telemetry-event.util/uwt-telemetry-event.util.ts +++ b/projects/ngx-ui-watchtower/src/lib/utils/uwt-telemetry-event.util/uwt-telemetry-event.util.ts @@ -8,8 +8,7 @@ import type { * What a sink was handed, told apart by kind — the result of * {@link uwtClassifyTelemetryEvent}. * - * `unknown` is an event type this library doesn't emit itself, such as a - * `UwtBIEventDetail.eventType` override. + * `unknown` is an event type this library doesn't emit itself. * * @group Types */ From 91fd57efc7c807350bdce7b1f48bc136ddb96989 Mon Sep 17 00:00:00 2001 From: fateeand Date: Fri, 16 Oct 2026 22:10:06 +0200 Subject: [PATCH 2/4] update DESIGN.md --- projects/ngx-ui-watchtower/DESIGN.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/projects/ngx-ui-watchtower/DESIGN.md b/projects/ngx-ui-watchtower/DESIGN.md index 2072e56..1f6a54d 100644 --- a/projects/ngx-ui-watchtower/DESIGN.md +++ b/projects/ngx-ui-watchtower/DESIGN.md @@ -168,8 +168,7 @@ the helper and the check. No destination at all still fails with `NG0201`. as a discriminated union — `scenario` (a `UwtScenarioRecord`), `scenario-step` (a `UwtScenarioStepEvent`), `bi` (a `UwtBIEvent`) or `unknown` (any other event type). It matches on the event type's ending, -so it works for any `eventNamespace` and for forwarded events; the broadcast -host uses it too. +so it works for any `eventNamespace` and for forwarded events. What a destination does **not** need to do: redact (payloads arrive already redacted), deduplicate BI events, time anything, or guard the library's @@ -2323,11 +2322,13 @@ and `location`, but not `top`, `BroadcastChannel` or storage — which is why this works there. **Where the shell owns the fragment's URL** — a plain iframe — a query -parameter does the same job: +parameter does the same job. The iframe must be same-origin with the shell, +like every fragment; a sourceless iframe (`about:blank`, `srcdoc`) qualifies +but has no URL to carry the parameter, so it reads the `top` global instead. ```ts // shell -iframe.src = `https://fragment.example.com/?channel=${channelId}`; +iframe.src = `/fragment/?channel=${channelId}`; // fragment const channelId = new URLSearchParams(window.location.search).get('channel'); From 5a087863909b733bea62b8bb8097afe2ed82a3b6 Mon Sep 17 00:00:00 2001 From: fateeand Date: Mon, 19 Oct 2026 10:23:16 +0200 Subject: [PATCH 3/4] update unit test --- .../uwt-bi-telemetry.service.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.spec.ts b/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.spec.ts index e8b2de5..8f13ede 100644 --- a/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.spec.ts +++ b/projects/ngx-ui-watchtower/src/lib/services/uwt-bi-telemetry.service/uwt-bi-telemetry.service.spec.ts @@ -665,6 +665,17 @@ describe('UwtBITelemetryService', () => { expect(sink.events[0].eventType).toBe('com.uwt.bi'); expect(sink.events[0].payload.eventName).toBe('click'); }); + + it('should collapse identical events that differ only in a legacy eventType', () => { + service.track('click', undefined, { + eventType: 'com.test-app.a' + } as never); + service.track('click', undefined, { + eventType: 'com.test-app.b' + } as never); + + expect(sink.events).toHaveLength(1); + }); }); describe('failure isolation', () => { From 6963a21fa38c0010b801479bea7dc4538fe4cc53 Mon Sep 17 00:00:00 2001 From: fateeand Date: Mon, 19 Oct 2026 12:48:59 +0200 Subject: [PATCH 4/4] update otel design --- projects/ngx-ui-watchtower/DESIGN.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/projects/ngx-ui-watchtower/DESIGN.md b/projects/ngx-ui-watchtower/DESIGN.md index 1f6a54d..f5c3a9a 100644 --- a/projects/ngx-ui-watchtower/DESIGN.md +++ b/projects/ngx-ui-watchtower/DESIGN.md @@ -2401,7 +2401,7 @@ whatever the destination. this.logger.emit({ eventName: eventType, body: event.payload.eventName, - attributes: { ...event.payload.metadata } + attributes: { ...event.payload.metadata, ...this.identity() } }); } } @@ -2412,7 +2412,8 @@ whatever the destination. startTime: new Date(start), attributes: { 'uwt.scenario.id': record.scenarioId, - 'uwt.scenario.status': record.status + 'uwt.scenario.status': record.status, + ...this.identity() } }); const parent = trace.setSpan(context.active(), root); @@ -2435,10 +2436,20 @@ whatever the destination. this.logger.emit({ eventName: 'exception', severityNumber: SeverityNumber.ERROR, - body: error.message + body: error.message, + attributes: this.identity() }); } + // Per record, not a resource attribute: resources are fixed at SDK startup, + // and the session changes on sign-out. + private identity(): Record { + return { + 'session.id': this.sessionId, + ...(this.userId ? { 'user.id': this.userId } : {}) + }; + } + getSessionId() { return this.sessionId; } @@ -2500,6 +2511,11 @@ whatever the destination. linked by `uwt.scenario.id` / `correlationId` — as it is with RUM today. Joining them by trace id would need scenarios to start spans in `start()`, which is a change to the core, not to the sink. +- **A session is one page load.** The sink's `sessionId` lives in memory, so a + reload or a second tab starts a new session — unlike RUM's cookie session, + which tabs share and reloads keep. Expect more, shorter sessions than RUM + reports; to match it, keep the id in a cookie with an inactivity expiry. There + is no anonymous user id either: until sign-in, only `session.id` is set. - **The browser logs SDK is still experimental.** Pin exact versions and re-run the tests after every upgrade: a mis-configured processor exports nothing, without an error.