Skip to content

Migrate Analytics Dashboard to domain-driven query API (Angular changes) - #36843

Open
jcastro-dotcms wants to merge 8 commits into
mainfrom
issue-36628-Implement-domain-driven-query-REST-API-for-Analytics-Dashboard-Angular-code-changes
Open

Migrate Analytics Dashboard to domain-driven query API (Angular changes)#36843
jcastro-dotcms wants to merge 8 commits into
mainfrom
issue-36628-Implement-domain-driven-query-REST-API-for-Analytics-Dashboard-Angular-code-changes

Conversation

@jcastro-dotcms

@jcastro-dotcms jcastro-dotcms commented Jul 31, 2026

Copy link
Copy Markdown
Member

Proposed Changes

  • Migrate all 7 DotAnalyticsService methods off the old per-metric endpoints (/event/*, /session/*, /conversion/content/attribution) onto the new domain-driven query API (/analytics/events, /analytics/sessions, /analytics/content), mapping each response back to the existing DTO shapes so no store/component code needed to change downstream
  • Remove getConversionsOverview() and the "Conversions Overview" table widget (component, store slice, types, i18n keys) from the Conversions tab entirely — the backend never built a domain-driven replacement for /analytics/conversion, since Product decided to drop that resource
  • Fix the dotCMS Java proxy allowlist (EventAnalyticsProxyHelper) to permit the new events/sessions/content path prefixes — without this, calls to the new endpoints 400 before reaching the upstream service
  • Change the dashboard's default landing tab from Pageview to Engagement (product decision)

Additional Changes (Not Part of the Original Ticket Requirements)

#36628 is scoped to the REST API migration itself. The two items below were separate asks/fixes that came up during implementation and review of this PR — flagging them explicitly since they go beyond that ticket's requirements:

  • Add a full-report loading overlay shown while a tab's data is loading, matching the look of dot-edit-content-layout's reload overlay
  • Fix analyticsHealthGuard from canMatch to canActivatecanMatch was re-firing the /api/v1/analytics/health check (and blocking navigation on its response) on every single tab click; canActivate only fires when entering/re-entering the Analytics Dashboard portlet, not on internal tab switches

Checklist

  • Tests
  • Translations
  • Security Implications Contemplated (add notes if applicable)

Additional Info

Related backend work (separate repo): dotCMS/dot-ca-event-manager#92, following the design/migration guide in that repo's docs/analytics-frontend-migration.md.

Migration was done gradually, one endpoint at a time: for each method, the exact outgoing request from the Angular dev build was captured and replayed against the old endpoint in Postman to diff the values before moving to the next.

Security note: the EventAnalyticsProxyHelper allowlist change only adds 3 new literal path-prefix strings (events, sessions, content) to an existing exact-match/prefix allowlist — it does not loosen the matching logic itself. Covered by existing + new unit tests in EventAnalyticsProxyHelperTest.

Translations: no new user-facing strings were added; only unused i18n keys tied to the removed Conversions Overview widget were deleted from the default Language.properties (no other locale files had these keys). The new loading overlay's aria-label reuses the existing generic Loading key.

Review follow-ups incorporated: dead-code cleanup (unused constant/type, dead store slice), a shared #queryEventsMetric helper to de-duplicate the aggregate/series query pattern, trimmed migration-era JSDoc down to enduring facts, guard redirect switched to returning a UrlTree instead of router.navigate() + false, a11y attributes on the loading overlay (role="status", aria-live="polite", aria-hidden on the decorative spinner), and moving the overlay's styling from custom SCSS to Tailwind utility classes per STYLING_STANDARDS.md.

Screenshots

No visual changes to chart/table rendering. User-visible changes are: default tab is now Engagement instead of Pageview, the Conversions tab no longer shows the "Conversions Overview" table, and a loading spinner overlay now appears briefly while a tab's data loads.

🤖 Generated with Claude Code

Migrates the Content Analytics dashboard's Angular code off the old
per-metric REST endpoints onto the new domain-driven query API
(/analytics/events, /analytics/sessions, /analytics/content), per the
backend redesign in dotCMS/dot-ca-event-manager#92.

- Migrate all 7 DotAnalyticsService methods to the new endpoints,
  mapping each response back to the existing DTO shapes so no
  store/component code needed to change
- Leave getConversionsOverview() on the old /conversion endpoint (no
  replacement exists) and remove the "Conversions Overview" table
  widget from the UI, matching the backend's decision to retire it
- Fix the dotCMS Java proxy allowlist (EventAnalyticsProxyHelper) to
  permit the new events/sessions/content path prefixes
- Change the dashboard's default landing tab from Pageview to
  Engagement (product decision)
- Add a full-report loading overlay shown while a tab's data loads,
  matching dot-edit-content-layout's reload-overlay look
- Fix analyticsHealthGuard from canMatch to canActivate — canMatch was
  re-firing the health check (and blocking navigation) on every tab
  click; canActivate only fires when entering/re-entering the portlet

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ST-API-for-Analytics-Dashboard-Angular-code-changes
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @jcastro-dotcms's task in 5m 56s —— View job


Code review — Analytics Dashboard domain-driven query migration

Reviewed the full diff (git diff origin/main...HEAD, 23 files). The prior review findings ($isReportLoading stuck overlay, isEnterprise snapshot param, missing store spec) are all correctly resolved, and nicobytes' 9 cleanup items are addressed. One new subtle correctness concern below.

New Issues

  • 🟡 Medium: core-web/libs/portlets/dot-analytics/portlet/src/lib/guards/analytics-health.guard.ts:37 — the guard reads the resolved isEnterprise value from route.data inside a canActivate guard, but Angular runs resolve after all canActivate/canActivateChild guards. On the navigation that enters /analytics/dashboard (or /search) from outside — the exact case canActivate was switched to fire on — the parent analytics route's DotEnterpriseLicenseResolver (app.routes.ts:78) has not run yet, so route.data['isEnterprise'] is undefined and falls through to ?? true. Result: a non-enterprise user who hits a failed/unavailable health check would be redirected to /analytics/error?isEnterprise=true, showing the wrong (enterprise) error variant instead of the licensing message.

    Assumption: Angular's documented guard→resolve ordering holds here (resolvers run after canActivate within a single navigation), and isEnterprise is resolved data, not static route data.

    What to verify: In a running dev build, log out of an enterprise license (or force DotEnterpriseLicenseResolver to return false), make the health check fail, and navigate into the dashboard fresh (full page load, not a tab switch). Confirm the error page receives isEnterprise=false. If it shows true, the resolved value isn't available at guard time and the flag needs to come from a source that's populated before canActivate (e.g. reading the license synchronously, or resolving on an ancestor that's already activated). Note: the guard spec (analytics-health.guard.spec.ts) sets route.data directly, so it can't catch this timing gap.

Existing (rollback risk — already flagged, non-blocking, tracked via label)

  • 🟡 Medium: dotCMS/.../EventAnalyticsProxyHelper.java:67 + core-web/.../services/dot-analytics.service.ts — frontend switched exclusively to events/sessions/content proxy paths in the same PR that adds them to the allowlist. A cached N bundle against a rolled-back N-1 backend gets 400s on every dashboard call. Correctly labeled AI: Not Safe To Rollback; additive allowlist is the right call. No code change needed — noting for release/rollback awareness.

Resolved

  • dot-analytics-dashboard.store.ts:29isUnsettled restricted to LOADING (not INIT); no-site early-returns no longer pin the overlay open.
  • analytics-health.guard.ts:42 — redirect now returns a UrlTree via createUrlTree instead of navigate() + false.
  • dot-analytics-dashboard.store.spec.ts — new spec covers both the no-site regression and the loading→settled transition.
  • with-conversions.feature.ts / analytics-api.types.ts / dot-analytics.constants.ts — dead conversionRate slice, AnalyticsEventResponse, ConversionOverviewData, and the unused ANALYTICS_CONVERSION* constants removed; no dangling references remain (grep clean).
  • dot-analytics-dashboard.component.html:52 — overlay a11y (role="status", aria-live, aria-hidden on spinner, aria-label) added; styling moved to Tailwind utilities.
  • dot-analytics.service.ts:406#queryEventsMetric de-dups the aggregate/series flow; null-safe (rows[0]?.[metric] ?? 0).

Everything else in the migration (response→DTO mapping, #buildDomainQueryParams comma-joined params, session-engagement rows[0] ?? {} guard, Java allowlist additivity + segment-based traversal check) looks correct.

@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code labels Jul 31, 2026
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / GraphQL / Headless API Contract Change
  • Risk Level: 🟡 MEDIUM
  • Why it's unsafe: This PR moves the Angular analytics dashboard to three new domain-driven REST resources (/api/v1/analytics/events, /api/v1/analytics/sessions, /api/v1/analytics/content) and the backend proxy allowlist is extended additively to permit them (event, conversion, session, health+ events, sessions, content). N-1's EventAnalyticsProxyHelper.ALLOWED_PATH_PREFIXES does NOT include events/sessions/content. DotAnalyticsService (core-web) now calls these new paths exclusively for every dashboard method (getTotalEvents, getUniqueVisitors, getContentAttribution, getTopContent, getPageviewsByDeviceBrowser, getSessionEngagement, getSessionEngagementGroupBy) — none of the old per-metric calls remain as a fallback. If a browser/CDN caches N's Angular bundle and the backend is then rolled back to N-1, EventAnalyticsProxyHelper.isAllowedRelativePath() rejects all of those calls with 400, breaking the entire analytics dashboard (Pageview, Conversions, and Engagement tabs) until the client re-fetches the N-1 bundle.
  • Code that makes it unsafe:
    • dotCMS/src/main/java/com/dotcms/rest/api/v1/analytics/event/EventAnalyticsProxyHelper.javaALLOWED_PATH_PREFIXES (adds "events", "sessions", "content", but N-1 lacks these three entries)
    • core-web/libs/portlets/dot-analytics/data-access/src/lib/services/dot-analytics.service.ts — all methods now call ANALYTICS_EVENTS_URL / ANALYTICS_SESSIONS_URL / ANALYTICS_CONTENT_URL instead of the old #EVENT_URL/#SESSION_URL/ANALYTICS_CONVERSION_URL paths, with no fallback to the old endpoints
  • Alternative (if possible): Per the M-3 guidance, this is a case where the frontend and backend contract changed together in the same PR. Since the backend proxy change is already additive (old prefixes kept), the main residual risk is the cached-frontend scenario — consider documenting this rollback risk in release notes so ops knows to force a hard client cache-bust (or confirm Angular's content-hashed bundle + non-cached index.html already forces a fresh fetch on redeploy) before/after a rollback of this release.

…ST-API-for-Analytics-Dashboard-Angular-code-changes

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the Angular Analytics Dashboard data-access layer from legacy per-metric analytics proxy endpoints to the newer domain-driven query API, while keeping downstream store/component DTO contracts stable. It also aligns the UI with the backend retirement of the Conversions Overview resource and improves navigation/loading behavior in the dashboard.

Changes:

  • Update DotAnalyticsService to query /api/v1/analytics/events|sessions|content and map the unified tabular envelope back into existing DTO shapes.
  • Remove the “Conversions Overview” widget end-to-end (component, store slice, types, i18n keys) and adjust the Conversions report layout accordingly.
  • Update routing/UX: switch analytics health guard to canActivate, change default dashboard landing tab to Engagement, and add a full-report loading overlay.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
dotCMS/src/test/java/com/dotcms/rest/api/v1/analytics/event/EventAnalyticsProxyHelperTest.java Adds unit coverage for allowing new domain-driven proxy paths (events, sessions, content).
dotCMS/src/main/webapp/WEB-INF/messages/Language.properties Removes unused i18n keys for the retired Conversions Overview widget.
dotCMS/src/main/java/com/dotcms/rest/api/v1/analytics/event/EventAnalyticsProxyHelper.java Extends proxy allowlist to permit the new domain-driven analytics resources.
core-web/libs/portlets/dot-analytics/portlet/src/lib/lib.routes.ts Switches analytics guard to canActivate and changes default dashboard redirect to engagement.
core-web/libs/portlets/dot-analytics/portlet/src/lib/guards/analytics-health.guard.ts Updates guard type/signature and documents why canActivate is preferred.
core-web/libs/portlets/dot-analytics/portlet/src/lib/guards/analytics-health.guard.spec.ts Updates tests to call the guard with activate snapshots/state.
core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/reports/conversions/dot-analytics-conversions-report/dot-analytics-conversions-report.component.ts Removes the Conversions Overview table slice/computed state usage.
core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/reports/conversions/dot-analytics-conversions-report/dot-analytics-conversions-report.component.html Removes the Conversions Overview table widget from the report layout.
core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/reports/conversions/dot-analytics-conversions-overview-table/dot-analytics-conversions-overview-table.component.ts Deletes the retired Conversions Overview table component.
core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/reports/conversions/dot-analytics-conversions-overview-table/dot-analytics-conversions-overview-table.component.spec.ts Deletes tests for the removed Conversions Overview component.
core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/reports/conversions/dot-analytics-conversions-overview-table/dot-analytics-conversions-overview-table.component.scss Deletes styles for the removed Conversions Overview component.
core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/reports/conversions/dot-analytics-conversions-overview-table/dot-analytics-conversions-overview-table.component.html Deletes template for the removed Conversions Overview component.
core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/dot-analytics-dashboard.component.scss Adds styles for a full-report loading overlay container/backdrop.
core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/dot-analytics-dashboard.component.html Adds a full-report loading overlay driven by store loading state.
core-web/libs/portlets/dot-analytics/data-access/src/lib/types/index.ts Exports new domain-driven query response types.
core-web/libs/portlets/dot-analytics/data-access/src/lib/types/analytics-domain-api.types.ts Adds unified tabular envelope types for the new query resources.
core-web/libs/portlets/dot-analytics/data-access/src/lib/types/analytics-api.types.ts Removes Conversions Overview types tied to the retired endpoint/widget.
core-web/libs/portlets/dot-analytics/data-access/src/lib/store/features/with-pageview.feature.ts Adjusts pageview breakdown calls to match updated service/query behavior.
core-web/libs/portlets/dot-analytics/data-access/src/lib/store/features/with-conversions.feature.ts Removes conversions overview state/loading method and stops triggering it.
core-web/libs/portlets/dot-analytics/data-access/src/lib/store/dot-analytics-dashboard.store.ts Adds computed $isReportLoading to coordinate full-report loading overlay per tab.
core-web/libs/portlets/dot-analytics/data-access/src/lib/services/dot-analytics.service.ts Migrates multiple analytics methods to new domain-driven query endpoints and adds shared param builder.
core-web/libs/portlets/dot-analytics/data-access/src/lib/services/dot-analytics.service.spec.ts Updates HTTP expectations and response fixtures to the new unified envelope and endpoints.
core-web/libs/portlets/dot-analytics/data-access/src/lib/constants/dot-analytics.constants.ts Adds new domain-driven endpoint constants and documents conversions overview retirement.

Comment on lines 20 to 23
export const analyticsHealthGuard: CanActivateFn = (_route, _state) => {
const analyticsService = inject(DotAnalyticsService);
const router = inject(Router);
const activatedRoute = inject(ActivatedRoute);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — you're right. isEnterprise is resolved on the parent analytics route (via DotEnterpriseLicenseResolver in app.routes.ts) and Angular merges resolved data down the ancestor chain unconditionally, so it's already present on this guard's own route snapshot param. Injecting ActivatedRoute instead risked resolving to whatever route was active before this navigation, since the target route's own instance isn't created until after guards pass — exactly the case that matters most now that this guard runs on canActivate (cross-portlet entry) rather than canMatch. Fixed in 4ce129a: reading from the snapshot param, dropped the now-unused ActivatedRoute injection, and updated the guard's tests accordingly.

jcastro-dotcms and others added 2 commits July 31, 2026 10:57
nx format:check (Prettier) failed the PR pipeline: this file's Tailwind
classes weren't in the plugin's canonical sort order. Pure class-reorder,
no structural/behavioral change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ven-query-REST-API-for-Analytics-Dashboard-Angular-code-changes' into issue-36628-Implement-domain-driven-query-REST-API-for-Analytics-Dashboard-Angular-code-changes
jcastro-dotcms and others added 2 commits July 31, 2026 11:33
Per Copilot review on #36843: inject(ActivatedRoute) inside a
CanActivateFn can resolve to whatever route was active *before* this
navigation, since the target route's own ActivatedRoute instance isn't
created until after guards pass. Read isEnterprise from the snapshot
parameter instead — Angular merges resolved `data` down the ancestor
chain unconditionally, so it's already there correctly (isEnterprise
is resolved on the parent 'analytics' route in app.routes.ts).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per automated PR review on #36843: isUnsettled treated
ComponentStatus.INIT as "loading". When currentSiteId is empty,
loadAllPageviewData()/loadConversionsData()/loadEngagementData() all
return without firing any request, leaving their slices at INIT
forever — nothing was ever going to resolve that status, so the new
full-report overlay would stay up indefinitely with no site selected.

Restrict isUnsettled to LOADING only. A real load transitions to
LOADING synchronously the instant it starts (patchState happens before
any network round trip), so the overlay still covers the actual ~2s
wait for an in-flight load; it just no longer hangs when nothing was
asked to load.

Also adds dot-analytics-dashboard.store.spec.ts (previously no test
file existed for this store), covering the regression case directly
plus the normal loading -> settled transition for $isReportLoading.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jcastro-dotcms

Copy link
Copy Markdown
Member Author

Thanks for the review — addressed both new findings:

  1. $isReportLoading stuck forever with no site — confirmed, this was a real regression. isUnsettled now only treats LOADING as unsettled (not INIT), so the no-site branches in loadAllPageviewData/loadConversionsData/loadEngagementData — which never fire a request and leave slices at INIT — no longer keep the overlay up indefinitely. A real load still transitions to LOADING synchronously the instant it starts, so the overlay still covers the actual in-flight wait. Fixed in 9240b6c.
  2. isEnterprise/ActivatedRoute guard issue — already fixed in 4ce129a (in response to Copilot's comment on the same line), confirming your independent recheck.
  3. No test coverage for $isReportLoading — added dot-analytics-dashboard.store.spec.ts (this store had no spec file at all before) in 9240b6c, covering the no-site regression case directly plus the normal loading→settled transition.

All three commits pushed to this branch.

@jcastro-dotcms

Copy link
Copy Markdown
Member Author

@claude regarding your feedback:

All three findings from your review were verified and addressed:

  1. $isReportLoading can leave the overlay spinning forever when no site is selected (Medium). Confirmed as a real regression. isUnsettled treated ComponentStatus.INIT as "loading", and when currentSiteId is empty, loadAllPageviewData()/loadConversionsData()/loadEngagementData() all return without firing any request, leaving their slices at INIT with nothing ever going to resolve that status. Fix: restricted isUnsettled to ComponentStatus.LOADING only. A real load still transitions to LOADING synchronously the instant it starts (before any network round trip), so the overlay still covers the actual in-flight wait — it just no longer hangs when nothing was asked to load. Fixed in commit 9240b6cee6.

  2. isEnterprise read from an injected ActivatedRoute instead of the guard's own snapshot param (Medium). Confirmed — same root cause you and Copilot both independently identified: inject(ActivatedRoute) inside a CanActivateFn can resolve against the route active before the navigation, since the target route's own ActivatedRoute instance isn't created until after guards pass. Fix: read isEnterprise from the guard's route snapshot parameter instead (Angular merges resolved data down the ancestor chain unconditionally, so it's already there — isEnterprise is resolved on the parent analytics route via DotEnterpriseLicenseResolver), and dropped the now-unused ActivatedRoute injection. This was fixed in commit 4ce129ab29 (in response to Copilot's identical comment on the same line, before your review posted).

  3. No test coverage for $isReportLoading (Medium). Confirmed — this store had no spec file at all before this PR. Added dot-analytics-dashboard.store.spec.ts with two tests: one reproducing the no-site regression from finding update with latest SVN #1 directly (asserts $isReportLoading() stays false when currentSiteId is empty), and one covering the normal loading → settled transition (asserts it flips true while a request is in flight and false once it resolves). Fixed in commit 9240b6cee6.

The non-blocking notes (Conversions Overview removal, allowlist additivity, the getSessionEngagement/toSessionEngagementData day-field observations) were also reviewed and confirmed accurate — no changes needed there.

All changes verified: full test suites pass, lint clean, nx format:check clean, and tsc --noEmit shows no type errors in any touched file.

@nicobytes
nicobytes self-requested a review July 31, 2026 19:15
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @jcastro-dotcms's task in 44s —— View job


Verified — all three fixes confirmed correct

  • Confirm isUnsettled fix in dot-analytics-dashboard.store.ts (commit 9240b6ce)
  • Confirm isEnterprise snapshot-param fix in analytics-health.guard.ts (commit 4ce129ab)
  • Confirm new dot-analytics-dashboard.store.spec.ts covers the no-site regression
  • Post final confirmation

1. $isReportLoading stuck-overlay fixdot-analytics-dashboard.store.ts:29-30 now defines isUnsettled as request.status === ComponentStatus.LOADING only (dropped INIT). Matches your description exactly: the no-currentSiteId early-returns in loadAllPageviewData/loadConversionsData/loadEngagementData leave slices at INIT with no in-flight request to ever resolve them, so excluding INIT from "unsettled" is the right fix, and a real load still flips to LOADING synchronously on kick-off so the overlay still covers genuine in-flight waits.

2. isEnterprise guard fixanalytics-health.guard.ts:37 now reads route.data?.['isEnterprise'] from the guard's own CanActivateFn route-snapshot parameter, with the now-unused ActivatedRoute injection removed. Correct: at canActivate time the target route's own ActivatedRoute instance doesn't exist yet, so inject(ActivatedRoute) risked resolving against the previously-active route; the snapshot param carries the resolved data merged down from the parent analytics route's DotEnterpriseLicenseResolver regardless.

3. Test coveragedot-analytics-dashboard.store.spec.ts adds two focused tests: one asserting $isReportLoading() stays false with no currentSiteId (direct regression coverage for finding #1), and one driving a real getSessionEngagement request through a Subject to assert the loading→settled transition (true while in flight, false after complete()). Both are meaningful, non-trivial assertions on the actual computed signal, not shallow smoke tests.

No further changes needed — closing this out.

@nicobytes nicobytes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: code simplification + Angular

Net positive migration (−1017 / +710). Domain-driven query API is encapsulated in DotAnalyticsService, DTO contracts stay stable, Conversions Overview removal is clean, and the canActivate + $isReportLoading fixes address real UX bugs.

Verdict: approve-with-follow-ups (non-blocking). Inline notes cover dead-code cleanup, service duplication/comment debt, and Angular guard/a11y polish.

Already solid

  • Shared #buildDomainQueryParams + toSessionEngagementData
  • Guard switched from canMatchcanActivate (correct Router lifecycle)
  • Overlay driven by store computed $isReportLoading (right layer)
  • Regression coverage for no-site overlay stuck state

Nit: PR description

Body still says getConversionsOverview() was left wired to the old endpoint — in the diff it was removed entirely. Worth updating for future readers.

@@ -9,12 +9,17 @@ export const ANALYTICS_CONVERSION_CONTENT_ATTRIBUTION_URL =
'/api/v1/analytics/conversion/content/attribution' as const;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplification — dead constant

ANALYTICS_CONVERSION_CONTENT_ATTRIBUTION_URL is no longer imported anywhere after the migration to ANALYTICS_CONTENT_URL. Safe to delete this export (and the JSDoc above it) in a small follow-up so we don't leave a stale proxy path in the public constants surface.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed dead — removed ANALYTICS_CONVERSION_CONTENT_ATTRIBUTION_URL and its JSDoc in 4adc8a9.

@@ -81,8 +78,7 @@ const initialConversionsState: ConversionsState = {
conversionRate: createInitialRequestState(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplification — dead store slice

conversionRate: RequestState<number> is still initialized/reset here, but nothing loads it and the Conversions report computes the rate locally from convertingVisitors. Leftover from before the overview removal — drop the field from ConversionsState, initialConversionsState, and this reset block.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed dead — removed conversionRate from ConversionsState, initialConversionsState, and the no-site reset block in 4adc8a9.

}

/** Response wrapper from analytics event endpoints (entity.data field) */
export interface AnalyticsEventResponse<T> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplification — unused legacy type

AnalyticsEventResponse<T> modeled the old entity.data envelope. The service now consumes AnalyticsQueryResponse (rows). Nothing imports this anymore — delete it (and update any stale JSDoc in this file that still references /event/total-events, /event/top-content, etc.).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed unused — deleted AnalyticsEventResponse<T> and fixed the stale JSDoc across this file that still referenced /event/total-events, /event/top-content, /event/pageviews-by-device-browser, /session/engagement, and /conversion/content/attribution (now describing the current DotAnalyticsService methods instead of the retired proxy paths). Done in 4adc8a9.

params: GetUniqueVisitorsParams
): Observable<UniqueVisitorsData | UniqueVisitorsByDayData[]> {
const httpParams = this.#buildUniqueVisitorsParams(params);
const httpParams = this.#buildDomainQueryParams({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplification — duplicated aggregate/series flow

getTotalEvents and getUniqueVisitors are the same shape: build params → GET /analytics/events → map rows[0] vs map series with dimensions=granularity. Consider a private helper (e.g. #queryEventsMetric) so the next metric doesn't copy-paste this again. Same idea applies lightly to getPageviewsByDeviceBrowser (device vs browser only renames the dimension field).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted a private #queryEventsMetric<K>(metric, params) helper — getTotalEvents/getUniqueVisitors are now one-line delegations. Structural typing worked out cleanly so no cast was needed at the call sites (K infers as the literal metric name, e.g. Record<'totalEvents', number> is structurally TotalEventsData).

Held off on getPageviewsByDeviceBrowser per your "lightly" framing — the device/browser duplication there is really just one output key name differing (2 lines), and unifying it would need a mapped-type generic ({ [P in K]: string } & { total: number }) that seemed like more type complexity than the duplication warranted. Happy to do it too if you'd rather have it unified for consistency. Done in 4adc8a9.

*
* @param params - Date range plus optional `siteId` and `eventType` query params
*/
getTopContent(params: GetRangeSiteEventParams): Observable<TopContentData[]> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplification — JSDoc length

The migration "why" notes on methods like getTopContent / getContentAttribution / getSessionEngagementGroupBy are useful in the PR, but as permanent service docs they're heavy (mode resolution, Postman confirmation, field renames). Prefer 1–2 lines of enduring intent (e.g. "restrict metrics to these 4 or grouped queries 400") and keep the archaeology in the PR / migration guide.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimmed getTopContent/getContentAttribution/getSessionEngagementGroupBy (and lightly getTotalEvents/getUniqueVisitors, now mostly documented on the shared #queryEventsMetric helper instead) down to 1-2 lines of enduring intent — dropped the mode-resolution deep-dive, the Postman-confirmation note, and the "old caller never did this" framing, kept the hard constraints (e.g. the 400-risk on getSessionEngagementGroupBy's metric restriction, the eventType-as-dimension mode flip on getTopContent). Done in 4adc8a9.

// unconditionally, so it's already present here correctly.
const isEnterprise = route.data?.['isEnterprise'] ?? true;

router.navigate(['/analytics/error'], {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Angular — prefer UrlTree over navigate + false

canActivate change is correct ✅. While here: Angular's preferred pattern is to return the redirect instead of side-effecting:

return router.createUrlTree(['/analytics/error'], {
  queryParams: { status: healthStatus, isEnterprise }
});

Avoids a separate navigate call, plays nicer with cancellation, and makes the guard test assert the tree directly. Pre-existing smell, but this file already moved — good moment to clean it up.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — the guard now returns router.createUrlTree(['/analytics/error'], {...}) instead of calling router.navigate() then returning false. Updated the spec to mock createUrlTree (echoing its args back) and assert the guard's return value directly instead of asserting a navigate side-effect. Done in 4adc8a9.

the per-widget skeletons underneath. -->
@if (store.$isReportLoading()) {
<div
class="analytics-dashboard-body__loading-overlay flex items-center justify-center"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Angular / a11y — loading overlay

Visual parity with edit-content's reload overlay is good. For WCAG AA, consider announcing the busy state:

  • role="status" + aria-live="polite" (or aria-busy on .analytics-dashboard-body)
  • aria-hidden="true" on the decorative spinner icon
  • optional aria-label via | dm

Non-blocking — matches the reference overlay — but worth a small polish since this is new UI.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added role="status", aria-live="polite", and aria-hidden="true" on the decorative spinner icon. Also added an aria-label (reusing the existing generic Loading i18n key rather than adding a new one) since without it the live region has no accessible name to announce — the icon is the only content and it's now hidden from AT. Done in 4adc8a9.

position: relative;
}

.analytics-dashboard-body__loading-overlay {

@nicobytes nicobytes Jul 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Angular / styling — prefer Tailwind utilities

Per project standards (STYLING_STANDARDS.md / Tailwind-first), this overlay does not need custom SCSS. Move these to the template on the overlay div:

class="analytics-dashboard-body__loading-overlay absolute inset-0 z-10 flex items-center justify-center bg-white/60"

Then drop this rule (and ideally the parent position: relative if you can put relative on .analytics-dashboard-body via Tailwind in the HTML too). Keep SCSS only when PrimeNG/::ng-deep forces it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to Tailwind utilities on the template (relative on .analytics-dashboard-body, absolute inset-0 z-10 flex items-center justify-center bg-white/60 on the overlay), dropped both SCSS rules entirely — kept the BEM class names for the data-testid/selector hooks per your snippet. Done in 4adc8a9.

let spectator: SpectatorService<InstanceType<typeof DotAnalyticsDashboardStore>>;
let store: InstanceType<typeof DotAnalyticsDashboardStore>;
let analyticsService: SpyObject<DotAnalyticsService>;
let globalStore: SpyObject<GlobalStore>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript — GlobalStore is a value, not a type

'GlobalStore' refers to a value, but is being used as a type here. Did you mean 'typeof GlobalStore'?

GlobalStore is a Signal Store (runtime value), same pattern as DotAnalyticsDashboardStore above. Fix:

let globalStore: SpyObject<InstanceType<typeof GlobalStore>>;

(or mirror whatever typing other portlet store specs use for GlobalStore).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, real compile error (ts-jest transpile-only mode doesn't catch it, only a real tsc run does — same lesson from a type error caught in a build earlier in this PR). Fixed: SpyObject<InstanceType<typeof GlobalStore>>. Verified with tsc --noEmit directly this time, not just the test run. Done in 4adc8a9.

Per human review on #36843:

- Remove dead ANALYTICS_CONVERSION_CONTENT_ATTRIBUTION_URL constant
  and unused AnalyticsEventResponse<T> type; fix stale JSDoc still
  referencing old /event/*, /session/*, /conversion* proxy paths
- Remove dead conversionRate slice from ConversionsState — nothing
  loaded it; the Conversions report computes the rate locally
- Extract a shared #queryEventsMetric helper in DotAnalyticsService
  so getTotalEvents/getUniqueVisitors stop duplicating the
  build-params -> GET -> map-scalar-or-series shape
- Trim migration-era JSDoc (mode-resolution deep-dives, Postman
  confirmation notes, "old caller never did this" framing) down to
  1-2 lines of enduring intent per method
- analyticsHealthGuard: return router.createUrlTree(...) instead of
  calling router.navigate() then returning false — the
  Angular-preferred guard-redirect pattern
- Loading overlay: add role="status" + aria-live="polite" +
  aria-label (reusing the existing generic "Loading" key),
  aria-hidden on the decorative spinner icon; move its positioning/
  backdrop from custom SCSS to Tailwind utility classes per
  STYLING_STANDARDS.md
- Fix a real TS error in the new store spec: GlobalStore is a value
  (signal store), not a type — SpyObject<InstanceType<typeof
  GlobalStore>>, not SpyObject<GlobalStore>

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jcastro-dotcms

Copy link
Copy Markdown
Member Author

Thanks for the thorough review, @nicobytes! All 9 inline suggestions addressed and pushed in 4adc8a96b0 (replied individually on each thread) — dead code removed (unused constant, unused type, dead conversionRate slice, stale JSDoc referencing retired endpoints), #queryEventsMetric helper extracted, migration-archaeology JSDoc trimmed, guard switched to returning a UrlTree, a11y attributes added to the overlay, overlay styling moved from SCSS to Tailwind utilities, and a real GlobalStore type error in the new store spec fixed.

Also fixed the PR description nit — it still said getConversionsOverview() was left wired to the old endpoint; updated it to reflect that it (and the whole Conversions Overview widget) was removed entirely.

@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / GraphQL / Headless API Contract Change
  • Risk Level: 🟡 MEDIUM
  • Why it's unsafe: DotAnalyticsService (Angular admin frontend) was migrated in this same PR to call new domain-driven proxy paths — /api/v1/analytics/events, /api/v1/analytics/sessions, /api/v1/analytics/content — instead of the old per-metric paths (event/total-events, event/unique-visitors, event/top-content, event/pageviews-by-device-browser, session/engagement, conversion, conversion/content/attribution). The backend allowlist in EventAnalyticsProxyHelper only adds the new prefixes (events, sessions, content) — it does not (and cannot, since it lives only in N) exist in N-1. If a browser has N's JS bundle cached (or a tab stays open) across a rollback to N-1, its requests to events/sessions/content will be rejected with HTTP 400 by the N-1 EventAnalyticsProxyHelper.isAllowedRelativePath() check, breaking the analytics dashboard for that session until the page is reloaded. This is exactly the documented M-3 signal: "Any API change where the Angular frontend is updated in the same PR to use the new shape."
  • Code that makes it unsafe:
    • core-web/libs/portlets/dot-analytics/data-access/src/lib/services/dot-analytics.service.ts — every method (getTotalEvents, getUniqueVisitors, getContentAttribution, getTopContent, getPageviewsByDeviceBrowser, getSessionEngagement, getSessionEngagementGroupBy) switched from the old #EVENT_URL/#SESSION_URL/ANALYTICS_CONVERSION_URL/ANALYTICS_CONVERSION_CONTENT_ATTRIBUTION_URL endpoints to ANALYTICS_EVENTS_URL, ANALYTICS_SESSIONS_URL, ANALYTICS_CONTENT_URL.
    • core-web/libs/portlets/dot-analytics/data-access/src/lib/constants/dot-analytics.constants.ts lines 1-31 — old URL constants removed, new ones added.
    • dotCMS/src/main/java/com/dotcms/rest/api/v1/analytics/event/EventAnalyticsProxyHelper.java line 65 (ALLOWED_PATH_PREFIXES) — events, sessions, content added; this list only exists in N, not N-1.
  • Alternative (if possible): Two-phase rollout — ship the backend allowlist widening (events/sessions/content) in one release without switching the frontend, then switch DotAnalyticsService to the new endpoints in the following release once N-1 (the pre-allowlist release) is outside the rollback window. Alternatively, keep the old proxy paths' upstream handlers alive and have N-1's allowlist already be a superset (not applicable retroactively here, hence the two-phase approach).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Not Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants