diff --git a/core-web/libs/sdk/analytics/README.md b/core-web/libs/sdk/analytics/README.md index 2a595dfbf6a1..b42f5e4e88b1 100644 --- a/core-web/libs/sdk/analytics/README.md +++ b/core-web/libs/sdk/analytics/README.md @@ -26,6 +26,12 @@ Or include the script in your HTML page: ### Provider Setup +First, import the provider: + +```tsx +import { DotContentAnalyticsProvider } from '@dotcms/analytics'; +``` + Wrap your application with the `DotContentAnalyticsProvider`: ```tsx diff --git a/core-web/libs/sdk/analytics/package.json b/core-web/libs/sdk/analytics/package.json index 7c923b12a642..6b878359226e 100644 --- a/core-web/libs/sdk/analytics/package.json +++ b/core-web/libs/sdk/analytics/package.json @@ -21,8 +21,7 @@ "homepage": "https://github.com/dotCMS/core/tree/main/core-web/libs/sdk/analytics/README.md", "dependencies": { "analytics": "^0.8.14", - "react": "^18.2.0", - "@dotcms/react": "0.0.1-alpha.38" + "react": "^18.2.0" }, "peerDependencies": { "vite": "^5.0.0" diff --git a/core-web/libs/sdk/analytics/src/index.ts b/core-web/libs/sdk/analytics/src/index.ts index 51b7c686c605..04d85b2460cb 100644 --- a/core-web/libs/sdk/analytics/src/index.ts +++ b/core-web/libs/sdk/analytics/src/index.ts @@ -1,8 +1,2 @@ -// HOC for wrapping a component with content analytics capabilities -export * from './lib/react/components/withContentAnalytics'; - -// Hook for tracking analytics events -export * from './lib/react/hook/useAnalyticTracker'; - -// Provider for the DotContentAnalytics instance export * from './lib/react/components/DotContentAnalyticsProvider'; +export * from './lib/react/hook/useContentAnalytics'; diff --git a/core-web/libs/sdk/analytics/src/lib/dot-content-analytics.spec.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/dot-content-analytics.spec.ts similarity index 100% rename from core-web/libs/sdk/analytics/src/lib/dot-content-analytics.spec.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/dot-content-analytics.spec.ts diff --git a/core-web/libs/sdk/analytics/src/lib/dot-content-analytics.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/dot-content-analytics.ts similarity index 77% rename from core-web/libs/sdk/analytics/src/lib/dot-content-analytics.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/dot-content-analytics.ts index 75b6fcda7ab1..c74b29635037 100644 --- a/core-web/libs/sdk/analytics/src/lib/dot-content-analytics.ts +++ b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/dot-content-analytics.ts @@ -24,13 +24,29 @@ export class DotContentAnalytics { private constructor(config: DotContentAnalyticsConfig) { this.#config = config; - this.logger = new DotLogger(this.#config.debug, 'DotContentAnalytics'); + this.logger = new DotLogger(config.debug, 'DotContentAnalytics'); + + if (!config.apiKey) { + this.#initialized = false; + } } /** * Returns the singleton instance of DotContentAnalytics */ static getInstance(config: DotContentAnalyticsConfig): DotContentAnalytics { + if (!config.apiKey) { + console.error( + `DotContentAnalytics: Missing "apiKey" in configuration - Events will not be sent to Content Analytics` + ); + } + + if (!config.server) { + console.error( + `DotContentAnalytics: Missing "server" in configuration - Events will not be sent to Content Analytics` + ); + } + if (!DotContentAnalytics.instance) { DotContentAnalytics.instance = new DotContentAnalytics(config); } @@ -52,10 +68,12 @@ export class DotContentAnalytics { this.logger.group('Initialization'); this.logger.time('Init'); + const plugins = this.#getPlugins(); + this.#analytics = Analytics({ app: 'dotAnalytics', debug: this.#config.debug, - plugins: [dotAnalyticsEnricherPlugin, dotAnalytics(this.#config)] + plugins }); this.#initialized = true; @@ -70,6 +88,19 @@ export class DotContentAnalytics { } } + /** + * Returns the plugins to be used in the analytics instance + */ + #getPlugins() { + const hasRequiredConfig = this.#config.apiKey && this.#config.server; + + if (!hasRequiredConfig) { + return []; + } + + return [dotAnalyticsEnricherPlugin, dotAnalytics(this.#config)]; + } + /** * Sends a page view event to the analytics instance. * diff --git a/core-web/libs/sdk/analytics/src/lib/plugin/dot-analytics.enricher.plugin.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/plugin/dot-analytics.enricher.plugin.ts similarity index 100% rename from core-web/libs/sdk/analytics/src/lib/plugin/dot-analytics.enricher.plugin.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/plugin/dot-analytics.enricher.plugin.ts diff --git a/core-web/libs/sdk/analytics/src/lib/plugin/dot-analytics.plugin.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/plugin/dot-analytics.plugin.ts similarity index 87% rename from core-web/libs/sdk/analytics/src/lib/plugin/dot-analytics.plugin.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/plugin/dot-analytics.plugin.ts index 3916c2c9c431..cae5dcdae608 100644 --- a/core-web/libs/sdk/analytics/src/lib/plugin/dot-analytics.plugin.ts +++ b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/plugin/dot-analytics.plugin.ts @@ -1,6 +1,6 @@ import { sendAnalyticsEventToServer } from '../shared/dot-content-analytics.http'; import { - DotAnalyticsPayload, + DotAnalyticsParams, DotContentAnalyticsConfig } from '../shared/dot-content-analytics.model'; @@ -23,10 +23,7 @@ export const dotAnalytics = (config: DotContentAnalyticsConfig) => { /** * Initialize the plugin */ - initialize: (params: { - config: DotContentAnalyticsConfig; - payload: DotAnalyticsPayload; - }) => { + initialize: (params: DotAnalyticsParams) => { const { config, payload } = params; if (config.debug) { console.warn('DotAnalytics: Initialized with config', config); @@ -50,7 +47,7 @@ export const dotAnalytics = (config: DotContentAnalyticsConfig) => { /** * Track a page view event */ - page: (params: { config: DotContentAnalyticsConfig; payload: DotAnalyticsPayload }) => { + page: (params: DotAnalyticsParams) => { const { config, payload } = params; if (!isInitialized) { @@ -68,7 +65,7 @@ export const dotAnalytics = (config: DotContentAnalyticsConfig) => { /** * Track a custom event */ - track: (params: { config: DotContentAnalyticsConfig; payload: DotAnalyticsPayload }) => { + track: (params: DotAnalyticsParams) => { const { config, payload } = params; if (!isInitialized) { diff --git a/core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.constants.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.constants.ts similarity index 100% rename from core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.constants.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.constants.ts diff --git a/core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.http.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.http.ts similarity index 82% rename from core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.http.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.http.ts index 6e2d384a9611..6974c59d423c 100644 --- a/core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.http.ts +++ b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.http.ts @@ -22,11 +22,17 @@ export const sendAnalyticsEventToServer = async ( } try { - return await fetch(`${options.server}${ANALYTICS_ENDPOINT}`, { + const response = await fetch(`${options.server}${ANALYTICS_ENDPOINT}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(serverEvent) }); + + if (response.ok) { + return response; + } else { + throw new Error(`${response.status}`); + } } catch (error) { console.error('DotAnalytics: Error sending event:', error); throw error; diff --git a/core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.model.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.model.ts similarity index 94% rename from core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.model.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.model.ts index 9ceebc126471..43604bfb503f 100644 --- a/core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.model.ts +++ b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.model.ts @@ -150,6 +150,14 @@ export interface ServerEvent extends Record { /** * Interface for the AnalyticsTracker. */ -export interface AnalyticsTracker { +export interface DotContentAnalyticsCustomHook { track: (eventName: string, payload?: Record) => void; } + +/** + * Params for the DotAnalytics plugin + */ +export interface DotAnalyticsParams { + config: DotContentAnalyticsConfig; + payload: DotAnalyticsPayload; +} diff --git a/core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.utils.spec.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.utils.spec.ts similarity index 100% rename from core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.utils.spec.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.utils.spec.ts diff --git a/core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.utils.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.utils.ts similarity index 100% rename from core-web/libs/sdk/analytics/src/lib/shared/dot-content-analytics.utils.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/shared/dot-content-analytics.utils.ts diff --git a/core-web/libs/sdk/analytics/src/lib/utils/DotLogger.ts b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/utils/DotLogger.ts similarity index 96% rename from core-web/libs/sdk/analytics/src/lib/utils/DotLogger.ts rename to core-web/libs/sdk/analytics/src/lib/dotAnalytics/utils/DotLogger.ts index 8c3a474bd17d..02e4cf33c977 100644 --- a/core-web/libs/sdk/analytics/src/lib/utils/DotLogger.ts +++ b/core-web/libs/sdk/analytics/src/lib/dotAnalytics/utils/DotLogger.ts @@ -2,6 +2,7 @@ * Logger for the dotCMS SDK */ export class DotLogger { + // TODO: Create a logger that can be used in the SDK private readonly isDebug: boolean; private readonly packageName: string; diff --git a/core-web/libs/sdk/analytics/src/lib/react/components/DotContentAnalyticsProvider.tsx b/core-web/libs/sdk/analytics/src/lib/react/components/DotContentAnalyticsProvider.tsx index e1c7f67a50a3..c844eb27f4ec 100644 --- a/core-web/libs/sdk/analytics/src/lib/react/components/DotContentAnalyticsProvider.tsx +++ b/core-web/libs/sdk/analytics/src/lib/react/components/DotContentAnalyticsProvider.tsx @@ -1,9 +1,9 @@ -import { ReactElement, ReactNode, useEffect, useState } from 'react'; +import { ReactElement, ReactNode, useEffect, useMemo } from 'react'; -import { DotContentAnalytics } from '../../dot-content-analytics'; -import { DotContentAnalyticsConfig } from '../../shared/dot-content-analytics.model'; +import { DotContentAnalytics } from '../../dotAnalytics/dot-content-analytics'; +import { DotContentAnalyticsConfig } from '../../dotAnalytics/shared/dot-content-analytics.model'; import DotContentAnalyticsContext from '../contexts/DotContentAnalyticsContext'; -import { useContentAnalytics } from '../hook/useContentAnalytics'; +import { useRouteTracker } from '../hook/useRouterTracker'; interface DotContentAnalyticsProviderProps { children?: ReactNode; @@ -25,17 +25,15 @@ export const DotContentAnalyticsProvider = ({ children, config }: DotContentAnalyticsProviderProps): ReactElement => { - const [instance, setInstance] = useState(null); - - useContentAnalytics(instance); + const instance = useMemo(() => DotContentAnalytics.getInstance(config), [config]); useEffect(() => { - const dotContentAnalyticsInstance = DotContentAnalytics.getInstance(config); - - dotContentAnalyticsInstance.ready().then(() => { - setInstance(dotContentAnalyticsInstance); + instance.ready().catch((err) => { + console.error('Error initializing analytics:', err); }); - }, []); + }, [instance]); + + useRouteTracker(instance); return ( diff --git a/core-web/libs/sdk/analytics/src/lib/react/components/withContentAnalytics.tsx b/core-web/libs/sdk/analytics/src/lib/react/components/withContentAnalytics.tsx deleted file mode 100644 index 1805e16c7c20..000000000000 --- a/core-web/libs/sdk/analytics/src/lib/react/components/withContentAnalytics.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { ReactNode, useCallback } from 'react'; - -import { DotcmsPageProps } from '@dotcms/react'; - -import { DotContentAnalyticsProvider } from './DotContentAnalyticsProvider'; - -import { DotContentAnalyticsConfig } from '../../shared/dot-content-analytics.model'; - -export interface PageProviderProps { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly entity: any; - readonly children: ReactNode; -} - -/** - * Wraps a given component with content analytics capabilities. - * This HOC adds analytics tracking functionality to the wrapped component, - * allowing it to automatically track page views and other analytics events. - * - * @param {React.ComponentType} WrappedComponent - The component to be enhanced with analytics. - * @param {DotContentAnalyticsConfig} config - Configuration for analytics, including API key, server URL and debug settings. - * @returns {React.FunctionComponent} A component that wraps the original component, - * adding analytics tracking based on the specified configuration. - */ -export const withContentAnalytics = ( - WrappedComponent: React.ComponentType, - config: DotContentAnalyticsConfig -) => { - return useCallback( - (props: DotcmsPageProps) => { - return ( - - - - ); - }, - [WrappedComponent] - ); -}; diff --git a/core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.spec.tsx b/core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.spec.tsx index 6e94aad388ee..257c60c17c82 100644 --- a/core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.spec.tsx +++ b/core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.spec.tsx @@ -1,10 +1,10 @@ import { jest } from '@jest/globals'; -import React, { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; import { ReactNode, useContext } from 'react'; import DotContentAnalyticsContext from './DotContentAnalyticsContext'; -import { DotContentAnalytics } from '../../dot-content-analytics'; +import { DotContentAnalytics } from '../../dotAnalytics/dot-content-analytics'; jest.mock('../dot-content-analytics', () => { return jest.fn().mockImplementation(() => { diff --git a/core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.tsx b/core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.tsx index e046ca9f4e1c..ad72cd10c8c0 100644 --- a/core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.tsx +++ b/core-web/libs/sdk/analytics/src/lib/react/contexts/DotContentAnalyticsContext.tsx @@ -1,6 +1,6 @@ import { createContext } from 'react'; -import { DotContentAnalytics } from '../../dot-content-analytics'; +import { DotContentAnalytics } from '../../dotAnalytics/dot-content-analytics'; /** * `DotContentAnalyticsContext` is a React context that is designed to provide an instance of diff --git a/core-web/libs/sdk/analytics/src/lib/react/hook/useAnalyticTracker.ts b/core-web/libs/sdk/analytics/src/lib/react/hook/useAnalyticTracker.ts deleted file mode 100644 index 372064825ecc..000000000000 --- a/core-web/libs/sdk/analytics/src/lib/react/hook/useAnalyticTracker.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useContext } from 'react'; - -import { AnalyticsTracker } from '../../shared/dot-content-analytics.model'; -import { isInsideEditor } from '../../shared/dot-content-analytics.utils'; -import DotContentAnalyticsContext from '../contexts/DotContentAnalyticsContext'; - -/** - * Hook to track analytics events in the application - * - * @returns {AnalyticsTracker} Object with track method to send events - * - * @example - * ```tsx - * const { track } = useAnalyticsTracker(); - * - * const handleClick = () => { - * track("btn-click", { - * title: "My Title", - * buttonText: "Link to detail", - * urlTitle: "my-page" - * }); - * }; - * - * return ( - * - * ); - * ``` - */ -export const useAnalyticsTracker = (): AnalyticsTracker => { - const instance = useContext(DotContentAnalyticsContext); - const insideEditor = isInsideEditor(); - - return { - track: (eventName: string, payload = {}) => { - if (instance?.track && !insideEditor) { - instance.track(eventName, { - ...payload, - timestamp: new Date().toISOString() - }); - } - } - }; -}; diff --git a/core-web/libs/sdk/analytics/src/lib/react/hook/useContentAnalytics.ts b/core-web/libs/sdk/analytics/src/lib/react/hook/useContentAnalytics.ts index c5fb5280ab3b..d22b8376dc6c 100644 --- a/core-web/libs/sdk/analytics/src/lib/react/hook/useContentAnalytics.ts +++ b/core-web/libs/sdk/analytics/src/lib/react/hook/useContentAnalytics.ts @@ -1,31 +1,33 @@ -import { useEffect } from 'react'; +import { useContext } from 'react'; -import { DotContentAnalytics } from '../../dot-content-analytics'; -import { isInsideEditor } from '../../shared/dot-content-analytics.utils'; +import { DotContentAnalyticsCustomHook } from '../../dotAnalytics/shared/dot-content-analytics.model'; +import { isInsideEditor } from '../../dotAnalytics/shared/dot-content-analytics.utils'; +import DotContentAnalyticsContext from '../contexts/DotContentAnalyticsContext'; /** * Custom hook that handles analytics page view tracking. * - * @param {DotContentAnalytics | null} instance - The analytics instance used to track page views - * @returns {void} + * @returns {DotContentAnalyticsCustomHook} - The analytics instance used to track page views * */ -export const useContentAnalytics = (instance: DotContentAnalytics | null): void => { - /** - * Tracks page view when component mounts, but only if: - * - We have a valid analytics instance - * - We're in a browser environment - * - We're not inside the editor - */ - useEffect(() => { - if (!instance || typeof window === 'undefined') { - return; - } - - const insideEditor = isInsideEditor(); +export const useContentAnalytics = (): DotContentAnalyticsCustomHook => { + const instance = useContext(DotContentAnalyticsContext); + const insideEditor = isInsideEditor(); - if (!insideEditor) { - instance.pageView({ source: 'headless' }); + return { + /** + * Track an event with the analytics instance. + * + * @param {string} eventName - The name of the event to track + * @param {object} payload - Additional data to include with the event + */ + track: (eventName: string, payload: Record = {}) => { + if (instance?.track && !insideEditor) { + instance.track(eventName, { + ...payload, + timestamp: new Date().toISOString() + }); + } } - }, [instance]); + }; }; diff --git a/core-web/libs/sdk/analytics/src/lib/react/hook/useRouterTracker.ts b/core-web/libs/sdk/analytics/src/lib/react/hook/useRouterTracker.ts new file mode 100644 index 000000000000..02adf329c473 --- /dev/null +++ b/core-web/libs/sdk/analytics/src/lib/react/hook/useRouterTracker.ts @@ -0,0 +1,25 @@ +import { useEffect, useRef } from 'react'; + +import { DotContentAnalytics } from '../../dotAnalytics/dot-content-analytics'; + +/** + * Internal custom hook that handles analytics page view tracking. + * + * @param {DotContentAnalytics | null} instance - The analytics instance used to track page views + * @returns {void} + * + */ +export function useRouteTracker(analytics: DotContentAnalytics | null) { + const lastPathRef = useRef(null); + + useEffect(() => { + if (!analytics) return; + + const currentPath = window.location.pathname; + + if (currentPath !== lastPathRef.current) { + lastPathRef.current = currentPath; + analytics.pageView(); + } + }, [analytics]); +} diff --git a/core-web/libs/sdk/analytics/src/lib/standalone.ts b/core-web/libs/sdk/analytics/src/lib/standalone.ts index d090c65e63dd..b7a14e8f3596 100644 --- a/core-web/libs/sdk/analytics/src/lib/standalone.ts +++ b/core-web/libs/sdk/analytics/src/lib/standalone.ts @@ -1,6 +1,6 @@ -import { DotContentAnalytics } from './dot-content-analytics'; -import { ANALYTICS_WINDOWS_KEY } from './shared/dot-content-analytics.constants'; -import { getDataAnalyticsAttributes } from './shared/dot-content-analytics.utils'; +import { DotContentAnalytics } from './dotAnalytics/dot-content-analytics'; +import { ANALYTICS_WINDOWS_KEY } from './dotAnalytics/shared/dot-content-analytics.constants'; +import { getDataAnalyticsAttributes } from './dotAnalytics/shared/dot-content-analytics.utils'; /** * Initialize the analytics library in standalone mode.