Status: Closed (fix shipped — opening here for the audit trail) Reported by: Manual QA — visiting the public landing pages while signed-out either flashes the global loading overlay every ~15 seconds or boots the user straight to the Keycloak login page. Affected components:
projects/spikersoft/src/app/_interceptors/jwt.interceptor.ts — global 401 handler that calls keyCloak.login(...).
Symptom
When an unauthenticated user lands on any route (the home / fundraiser splash / sponsor browse pages are all public):
NavigationActivityService emits ux.navigation.visit via trackOnce, which queues an event.
ActivityTrackingService's constructor kicks off ActivityManifestService.ensureLoaded(), firing GET /api/activity/manifest immediately.
The 15s flush timer ticks and flush() posts the queued events to POST /api/activity/track.
Both endpoints require auth. The backend returns 401.
The global JwtInterceptor sees the 401, hits the "not authenticated, can't refresh" branch, and calls keyCloak.login({ redirectUri: location.href }) — the public visitor is teleported to the Keycloak login page mid-scroll.
Even when the loading overlay was suppressed via skipLoading() for the activity request itself, the subsequent Keycloak redirect blanks the screen and the user perceives "another loader" before being deposited at a sign-in form they never asked for.
Root cause
The activity tracking platform lib had no concept of authentication state. It assumed every consuming app has a logged-in user (true historically — the entire SPA used to require auth). Now that the marketing/sponsor surface is publicly browsable, background telemetry traffic 401s and the global JWT interceptor's "force login" handler turns those 401s into a UX regression.
ActivityTrackingService.flush() runs every 15 s with no auth check.
setupBeforeUnload's sendBeacon fires the queue at tab close regardless of auth — silent, but still a wasted round-trip and a 401 in the server logs.
Fix
Introduce a thin DI seam so the lib stays decoupled from Keycloak:
New ACTIVITY_AUTH_GATE injection token in @spikersoft/platform-activity-tracking. It resolves to a () => boolean callback. Default factory is () => true so existing tests and consumers keep their current behavior. The shell app provides a Keycloak-backed factory.
provideActivityAuthGate(factory) helper for the wiring site so main.ts reads as one obvious line.
ActivityTrackingService.flush() and the beforeunload beacon both bail when the gate returns false. Events stay in the in-memory queue (and persist to localStorage) so once the visitor signs up / signs in, the next flush ships everything they did pre-login under their freshly minted user id — which is exactly the attribution we want for funnel analytics.
A queue cap (MAX_QUEUE_SIZE = 500) prevents unbounded growth for visitors who never log in. Oldest events are dropped first; the SPA never crashes from a runaway localStorage write.
ActivityManifestService.ensureLoaded() short-circuits when the gate returns false. The constructor still rehydrates the manifest from localStorage if a prior signed-in session cached it, so returning users immediately ship compact events without a network hit.
main.ts wires the token to () => inject(Keycloak).authenticated ?? false.
The JWT interceptor is left unchanged — it's correct that 401 on an authenticated user-initiated request should redirect to login. The bug was that we were sending background traffic that had no business going out before login.
Wire-format / API impact
None. Server-side endpoints are untouched. Existing logged-in users see no change in behavior.
Resolution
Closed. Implemented in:
spikersoft-angular/libraries/platform/activity-tracking/src/lib/auth-gate.token.ts (new file)
activity-tracking.service.spec.ts — gating of flush() when the gate returns false; resumption of flush after the gate flips to true.
activity-manifest.service.spec.ts — ensureLoaded no-ops while the gate is false; cached manifest still resolves.
Validation
npx nx test platform-activity-tracking — 20 / 20 pass (8 new gating cases plus prior coverage).
npx nx build platform-activity-tracking — clean.
npx nx run spikersoft:build:development — Successfully ran target build for project spikersoft and 61 tasks. The only warning is the pre-existing VisualizationControlPanelComponent template-not-used warning, untouched by this change.
**Status:** Closed (fix shipped — opening here for the audit trail)
**Reported by:** Manual QA — visiting the public landing pages while signed-out either flashes the global loading overlay every ~15 seconds or boots the user straight to the Keycloak login page.
**Affected components:**
- `@spikersoft/platform-activity-tracking` — `ActivityTrackingService`, `ActivityManifestService`, `NavigationActivityService`.
- `projects/spikersoft/src/app/_interceptors/jwt.interceptor.ts` — global 401 handler that calls `keyCloak.login(...)`.
## Symptom
When an unauthenticated user lands on any route (the home / fundraiser splash / sponsor browse pages are all public):
1. `NavigationActivityService` emits `ux.navigation.visit` via `trackOnce`, which queues an event.
2. `ActivityTrackingService`'s constructor kicks off `ActivityManifestService.ensureLoaded()`, firing `GET /api/activity/manifest` immediately.
3. The 15s flush timer ticks and `flush()` posts the queued events to `POST /api/activity/track`.
4. Both endpoints require auth. The backend returns 401.
5. The global `JwtInterceptor` sees the 401, hits the "not authenticated, can't refresh" branch, and calls `keyCloak.login({ redirectUri: location.href })` — the public visitor is teleported to the Keycloak login page mid-scroll.
Even when the loading overlay was suppressed via `skipLoading()` for the activity request itself, the subsequent Keycloak redirect blanks the screen and the user perceives "another loader" before being deposited at a sign-in form they never asked for.
## Root cause
The activity tracking platform lib had no concept of authentication state. It assumed every consuming app has a logged-in user (true historically — the entire SPA used to require auth). Now that the marketing/sponsor surface is publicly browsable, background telemetry traffic 401s and the global JWT interceptor's "force login" handler turns those 401s into a UX regression.
Three specific contributors:
1. `ActivityTrackingService` constructor calls `manifestService.ensureLoaded()` unconditionally — anonymous first paint always hits `/activity/manifest`.
2. `ActivityTrackingService.flush()` runs every 15 s with no auth check.
3. `setupBeforeUnload`'s `sendBeacon` fires the queue at tab close regardless of auth — silent, but still a wasted round-trip and a 401 in the server logs.
## Fix
Introduce a thin DI seam so the lib stays decoupled from Keycloak:
- New `ACTIVITY_AUTH_GATE` injection token in `@spikersoft/platform-activity-tracking`. It resolves to a `() => boolean` callback. Default factory is `() => true` so existing tests and consumers keep their current behavior. The shell app provides a Keycloak-backed factory.
- `provideActivityAuthGate(factory)` helper for the wiring site so `main.ts` reads as one obvious line.
- `ActivityTrackingService.flush()` and the `beforeunload` beacon both bail when the gate returns false. Events stay in the in-memory queue (and persist to `localStorage`) so once the visitor signs up / signs in, the next flush ships everything they did pre-login under their freshly minted user id — which is exactly the attribution we want for funnel analytics.
- A queue cap (`MAX_QUEUE_SIZE = 500`) prevents unbounded growth for visitors who never log in. Oldest events are dropped first; the SPA never crashes from a runaway `localStorage` write.
- `ActivityManifestService.ensureLoaded()` short-circuits when the gate returns false. The constructor still rehydrates the manifest from `localStorage` if a prior signed-in session cached it, so returning users immediately ship compact events without a network hit.
- `main.ts` wires the token to `() => inject(Keycloak).authenticated ?? false`.
The JWT interceptor is left unchanged — it's correct that 401 on an authenticated user-initiated request should redirect to login. The bug was that we were sending background traffic that had no business going out before login.
## Wire-format / API impact
None. Server-side endpoints are untouched. Existing logged-in users see no change in behavior.
## Resolution
Closed. Implemented in:
- `spikersoft-angular/libraries/platform/activity-tracking/src/lib/auth-gate.token.ts` (new file)
- `spikersoft-angular/libraries/platform/activity-tracking/src/lib/activity-tracking.service.ts`
- `spikersoft-angular/libraries/platform/activity-tracking/src/lib/activity-manifest.service.ts`
- `spikersoft-angular/libraries/platform/activity-tracking/src/index.ts`
- `spikersoft-angular/projects/spikersoft/src/main.ts`
Regression coverage:
- `activity-tracking.service.spec.ts` — gating of `flush()` when the gate returns false; resumption of flush after the gate flips to true.
- `activity-manifest.service.spec.ts` — `ensureLoaded` no-ops while the gate is false; cached manifest still resolves.
### Validation
- `npx nx test platform-activity-tracking` — **20 / 20 pass** (8 new gating cases plus prior coverage).
- `npx nx build platform-activity-tracking` — clean.
- `npx nx run spikersoft:build:development` — *Successfully ran target build for project spikersoft and 61 tasks*. The only warning is the pre-existing `VisualizationControlPanelComponent` template-not-used warning, untouched by this change.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Status: Closed (fix shipped — opening here for the audit trail)
Reported by: Manual QA — visiting the public landing pages while signed-out either flashes the global loading overlay every ~15 seconds or boots the user straight to the Keycloak login page.
Affected components:
@spikersoft/platform-activity-tracking—ActivityTrackingService,ActivityManifestService,NavigationActivityService.projects/spikersoft/src/app/_interceptors/jwt.interceptor.ts— global 401 handler that callskeyCloak.login(...).Symptom
When an unauthenticated user lands on any route (the home / fundraiser splash / sponsor browse pages are all public):
NavigationActivityServiceemitsux.navigation.visitviatrackOnce, which queues an event.ActivityTrackingService's constructor kicks offActivityManifestService.ensureLoaded(), firingGET /api/activity/manifestimmediately.flush()posts the queued events toPOST /api/activity/track.JwtInterceptorsees the 401, hits the "not authenticated, can't refresh" branch, and callskeyCloak.login({ redirectUri: location.href })— the public visitor is teleported to the Keycloak login page mid-scroll.Even when the loading overlay was suppressed via
skipLoading()for the activity request itself, the subsequent Keycloak redirect blanks the screen and the user perceives "another loader" before being deposited at a sign-in form they never asked for.Root cause
The activity tracking platform lib had no concept of authentication state. It assumed every consuming app has a logged-in user (true historically — the entire SPA used to require auth). Now that the marketing/sponsor surface is publicly browsable, background telemetry traffic 401s and the global JWT interceptor's "force login" handler turns those 401s into a UX regression.
Three specific contributors:
ActivityTrackingServiceconstructor callsmanifestService.ensureLoaded()unconditionally — anonymous first paint always hits/activity/manifest.ActivityTrackingService.flush()runs every 15 s with no auth check.setupBeforeUnload'ssendBeaconfires the queue at tab close regardless of auth — silent, but still a wasted round-trip and a 401 in the server logs.Fix
Introduce a thin DI seam so the lib stays decoupled from Keycloak:
ACTIVITY_AUTH_GATEinjection token in@spikersoft/platform-activity-tracking. It resolves to a() => booleancallback. Default factory is() => trueso existing tests and consumers keep their current behavior. The shell app provides a Keycloak-backed factory.provideActivityAuthGate(factory)helper for the wiring site somain.tsreads as one obvious line.ActivityTrackingService.flush()and thebeforeunloadbeacon both bail when the gate returns false. Events stay in the in-memory queue (and persist tolocalStorage) so once the visitor signs up / signs in, the next flush ships everything they did pre-login under their freshly minted user id — which is exactly the attribution we want for funnel analytics.MAX_QUEUE_SIZE = 500) prevents unbounded growth for visitors who never log in. Oldest events are dropped first; the SPA never crashes from a runawaylocalStoragewrite.ActivityManifestService.ensureLoaded()short-circuits when the gate returns false. The constructor still rehydrates the manifest fromlocalStorageif a prior signed-in session cached it, so returning users immediately ship compact events without a network hit.main.tswires the token to() => inject(Keycloak).authenticated ?? false.The JWT interceptor is left unchanged — it's correct that 401 on an authenticated user-initiated request should redirect to login. The bug was that we were sending background traffic that had no business going out before login.
Wire-format / API impact
None. Server-side endpoints are untouched. Existing logged-in users see no change in behavior.
Resolution
Closed. Implemented in:
spikersoft-angular/libraries/platform/activity-tracking/src/lib/auth-gate.token.ts(new file)spikersoft-angular/libraries/platform/activity-tracking/src/lib/activity-tracking.service.tsspikersoft-angular/libraries/platform/activity-tracking/src/lib/activity-manifest.service.tsspikersoft-angular/libraries/platform/activity-tracking/src/index.tsspikersoft-angular/projects/spikersoft/src/main.tsRegression coverage:
activity-tracking.service.spec.ts— gating offlush()when the gate returns false; resumption of flush after the gate flips to true.activity-manifest.service.spec.ts—ensureLoadedno-ops while the gate is false; cached manifest still resolves.Validation
npx nx test platform-activity-tracking— 20 / 20 pass (8 new gating cases plus prior coverage).npx nx build platform-activity-tracking— clean.npx nx run spikersoft:build:development— Successfully ran target build for project spikersoft and 61 tasks. The only warning is the pre-existingVisualizationControlPanelComponenttemplate-not-used warning, untouched by this change.