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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,9 @@ export const CONFIGURATION_CONFIRM_DIALOG_KEY = 'confirmDialog';
export enum HealthStatusTypes {
OK = 'OK',
NOT_CONFIGURED = 'NOT_CONFIGURED',
CONFIGURATION_ERROR = 'CONFIGURATION_ERROR'
CONFIGURATION_ERROR = 'CONFIGURATION_ERROR',
AVAILABLE = 'AVAILABLE',
NOT_AVAILABLE = 'NOT_AVAILABLE'
}

export const RUNNING_UNTIL_DATE_FORMAT = 'EEE, LLL dd';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export const TIME_RANGE_CUBEJS_MAPPING = {
last30days: 'from 30 days ago to now'
} as const;

/** Maps internal time range options to the new analytics event API `range` param */
export const TIME_RANGE_API_MAPPING: Record<string, string> = {
[TIME_RANGE_OPTIONS.last7days]: 'last_7_days',
[TIME_RANGE_OPTIONS.last30days]: 'last_30_days'
} as const;

/** Maps time range options to comparison label days count */
export const TIME_RANGE_DAYS_MAP: Record<string, number> = {
[TIME_RANGE_OPTIONS.last7days]: 7,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import { Observable } from 'rxjs';
import { Observable, of } from 'rxjs';

import { HttpClient } from '@angular/common/http';
import { HttpClient, HttpParams } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';

import { map } from 'rxjs/operators';
import { catchError, map, shareReplay } from 'rxjs/operators';

import { AnalyticsApiResponse, CubeJSQuery } from '../../index';
import { DotCMSResponse, HealthStatusTypes } from '@dotcms/dotcms-models';

import {
AnalyticsApiResponse,
AnalyticsEventResponse,
ApiGranularity,
ApiRangeParams,
CubeJSQuery,
DeviceBrowserData,
TopContentData,
TotalEventsByDayData,
TotalEventsData,
UniqueVisitorsByDayData,
UniqueVisitorsData
} from '../../index';

/**
* Generic analytics service for CubeJS queries.
*
* This service provides a single method to execute any CubeJS query.
* Query construction is handled by the store using the CubeQueryBuilder.
* Generic analytics service for CubeJS queries and health checks.
*
* @example
* ```typescript
Expand All @@ -22,7 +33,7 @@ import { AnalyticsApiResponse, CubeJSQuery } from '../../index';
* .siteId(siteId)
* .build();
*
* analyticsService.query<TotalPageViewsEntity>(query).pipe(
* analyticsService.cubeQuery<TotalPageViewsEntity>(query).pipe(
* map(entities => entities[0])
* );
* ```
Expand All @@ -32,8 +43,156 @@ import { AnalyticsApiResponse, CubeJSQuery } from '../../index';
})
export class DotAnalyticsService {
readonly #BASE_URL = '/api/v1/analytics/content/_query/cube';
readonly #EVENT_URL = '/api/v1/analytics/event';
readonly #HEALTH_URL = '/api/v1/analytics/check';
readonly #http = inject(HttpClient);

#healthCache$: Observable<HealthStatusTypes> | null = null;

/**
* Checks analytics availability via the health endpoint.
* Always makes a fresh HTTP request.
*
* @returns Observable of HealthStatusTypes (AVAILABLE or NOT_AVAILABLE)
*/
healthCheck(): Observable<HealthStatusTypes> {
return this.#http.get<{ available: string }>(this.#HEALTH_URL).pipe(
map((response) =>
response.available === 'true'
? HealthStatusTypes.AVAILABLE
: HealthStatusTypes.NOT_AVAILABLE
),
catchError(() => of(HealthStatusTypes.AVAILABLE))
);
Comment on lines +59 to +66
}

/**
* Cached version of healthCheck. Uses shareReplay to avoid
* multiple HTTP calls across guards/components in the same navigation.
*
* @returns Observable of HealthStatusTypes (cached)
*/
healthCheckWithCache(): Observable<HealthStatusTypes> {
if (!this.#healthCache$) {
this.#healthCache$ = this.healthCheck().pipe(shareReplay(1));
}

return this.#healthCache$;
}

/**
* Clears the cached health check result, forcing a fresh request on next call.
*/
clearHealthCache(): void {
this.#healthCache$ = null;
}

/**
* Fetches total events from the new analytics event endpoint.
* Supports predefined ranges (`?range=last_7_days`) or custom dates (`?from=...&to=...`).
*
* @param rangeParams - Object with either `range` or `from`+`to` query params
* @param granularity - Optional granularity (e.g. 'day', 'hour')
* @returns Observable of TotalEventsData (single object) or TotalEventsByDayData[] (array with granularity)
*/
getTotalEvents(rangeParams: ApiRangeParams): Observable<TotalEventsData>;
getTotalEvents(
rangeParams: ApiRangeParams,
granularity: ApiGranularity
): Observable<TotalEventsByDayData[]>;
getTotalEvents(
rangeParams: ApiRangeParams,
granularity?: ApiGranularity
): Observable<TotalEventsData | TotalEventsByDayData[]> {
let params = this.#buildRangeParams(rangeParams);
if (granularity) {
params = params.set('granularity', granularity);
}

return this.#http
.get<
DotCMSResponse<AnalyticsEventResponse<TotalEventsData | TotalEventsByDayData[]>>
>(`${this.#EVENT_URL}/total-events`, { params })
.pipe(map((response) => response.entity.data));
}

/**
* Fetches unique visitors from the new analytics event endpoint.
* Supports predefined ranges (`?range=last_7_days`) or custom dates (`?from=...&to=...`).
*
* @param rangeParams - Object with either `range` or `from`+`to` query params
* @param granularity - Optional granularity (e.g. 'day', 'hour')
* @returns Observable of UniqueVisitorsData (single object) or UniqueVisitorsByDayData[] (array with granularity)
*/
getUniqueVisitors(rangeParams: ApiRangeParams): Observable<UniqueVisitorsData>;
getUniqueVisitors(
rangeParams: ApiRangeParams,
granularity: ApiGranularity
): Observable<UniqueVisitorsByDayData[]>;
getUniqueVisitors(
rangeParams: ApiRangeParams,
granularity?: ApiGranularity
): Observable<UniqueVisitorsData | UniqueVisitorsByDayData[]> {
let params = this.#buildRangeParams(rangeParams);
if (granularity) {
params = params.set('granularity', granularity);
}

return this.#http
.get<
DotCMSResponse<
AnalyticsEventResponse<UniqueVisitorsData | UniqueVisitorsByDayData[]>
>
>(`${this.#EVENT_URL}/unique-visitors`, { params })
.pipe(map((response) => response.entity.data));
}

/**
* Fetches top content from the new analytics event endpoint.
* Returns an array of content items ordered by total events descending.
*
* @param rangeParams - Object with either `range` or `from`+`to` query params
* @returns Observable of TopContentData[]
*/
getTopContent(rangeParams: ApiRangeParams): Observable<TopContentData[]> {
const params = this.#buildRangeParams(rangeParams);

return this.#http
.get<
DotCMSResponse<AnalyticsEventResponse<TopContentData[]>>
>(`${this.#EVENT_URL}/top-content`, { params })
.pipe(map((response) => response.entity.data));
}

/**
* Fetches pageviews by device and browser from the new analytics event endpoint.
* Returns an array of items with browser, device, and total count.
*
* @param rangeParams - Object with either `range` or `from`+`to` query params
* @returns Observable of DeviceBrowserData[]
*/
getPageviewsByDeviceBrowser(rangeParams: ApiRangeParams): Observable<DeviceBrowserData[]> {
const params = this.#buildRangeParams(rangeParams);

return this.#http
.get<
DotCMSResponse<AnalyticsEventResponse<DeviceBrowserData[]>>
>(`${this.#EVENT_URL}/pageviews-by-device-browser`, { params })
.pipe(map((response) => response.entity.data));
}

#buildRangeParams(rangeParams: ApiRangeParams): HttpParams {
let params = new HttpParams();
if ('range' in rangeParams) {
params = params.set('range', rangeParams.range);
} else {
params = params.set('from', rangeParams.from);
params = params.set('to', rangeParams.to);
}

return params;
}

/**
* Executes a CubeJS query and returns the entity array.
*
Expand Down
Loading
Loading