diff --git a/openmetadata-ui/src/main/resources/ui/src/components/WebAnalytics/WebAnalytics.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/WebAnalytics/WebAnalytics.interface.ts index bebd0527dd7..8f3d32448d5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/WebAnalytics/WebAnalytics.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/WebAnalytics/WebAnalytics.interface.ts @@ -28,6 +28,7 @@ export interface Payload { type: string; properties: PayloadProperties; anonymousId: string; + event: string; meta: { rid: string; ts: number; @@ -35,6 +36,6 @@ export interface Payload { }; } -export interface WebPageData extends PageData { +export interface AnalyticsData extends PageData { payload: Payload; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/router/AppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/router/AppRouter.tsx index ba9332e950b..dbc4f6b1d2c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/router/AppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/router/AppRouter.tsx @@ -11,8 +11,9 @@ * limitations under the License. */ +import { CustomEventTypes } from 'generated/analytics/webAnalyticEventData'; import AccountActivationConfirmation from 'pages/signup/account-activation-confirmation.component'; -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; import { useAnalytics } from 'use-analytics'; import { ROUTES } from '../../constants/constants'; @@ -86,6 +87,24 @@ const AppRouter = () => { } }, [location.pathname]); + const handleClickEvent = useCallback( + (event: MouseEvent) => { + const eventValue = + (event.target as HTMLElement)?.textContent || CustomEventTypes.Click; + if (eventValue) { + analytics.track(eventValue); + } + }, + [analytics] + ); + + useEffect(() => { + const targetNode = document.body; + targetNode.addEventListener('click', handleClickEvent); + + return () => targetNode.removeEventListener('click', handleClickEvent); + }, [handleClickEvent]); + if (loading) { return ; } diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/WebAnalyticsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/WebAnalyticsAPI.ts index 8efade57928..2b32e0d034c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/WebAnalyticsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/WebAnalyticsAPI.ts @@ -15,7 +15,7 @@ import { AxiosResponse } from 'axios'; import { WebAnalyticEventData } from '../generated/analytics/webAnalyticEventData'; import APIClient from './index'; -export const postPageView = async ( +export const postWebAnalyticEvent = async ( webAnalyticEventData: WebAnalyticEventData ) => { const response = await APIClient.put< diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.test.ts index 8ecdae24342..797dfef8c5b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.test.ts @@ -11,10 +11,63 @@ * limitations under the License. */ -import { getAnalyticInstance, getReferrerPath } from './WebAnalyticsUtils'; +import { AnalyticsData } from 'components/WebAnalytics/WebAnalytics.interface'; +import { postWebAnalyticEvent } from 'rest/WebAnalyticsAPI'; +import { + getAnalyticInstance, + getReferrerPath, + trackCustomEvent, +} from './WebAnalyticsUtils'; const userId = 'userId'; +jest.mock('rest/WebAnalyticsAPI', () => ({ + postWebAnalyticEvent: jest.fn().mockImplementation(() => Promise.resolve()), +})); + +jest.mock('@analytics/session-utils', () => ({ + ...jest.requireActual('@analytics/session-utils'), + getSession: jest + .fn() + .mockReturnValue({ id: '19c85e4f-7679-4fba-813f-e72108d914c4' }), +})); + +const MOCK_ANALYTICS_DATA: AnalyticsData = { + payload: { + type: 'page', + properties: { + title: 'OpenMetadata', + url: 'http://localhost/', + path: '/', + hash: '', + search: '?page=1', + width: 1440, + height: 284, + referrer: 'http://localhost:3000/explore/tables?page=1', + }, + event: 'Explore', + meta: { + rid: '7a14e508-5cbc-4bf9-b922-0607a2ff2aa5', + ts: 1680246874535, + hasCallback: true, + }, + anonymousId: '7a14e508-5cbc-4bf9-b922-0607a2ff2aa5', + }, +}; + +const CUSTOM_EVENT_PAYLOAD = { + eventType: 'CustomEvent', + eventData: { + url: '/', + fullUrl: 'http://localhost/', + hostname: 'localhost', + eventType: 'CLICK', + sessionId: '19c85e4f-7679-4fba-813f-e72108d914c4', + eventValue: 'Explore', + }, + timestamp: 1680246874535, +}; + const mockReferrer = 'http://localhost:3000/settings/members/teams/Organization'; @@ -42,4 +95,10 @@ describe('Web Analytics utils', () => { expect(pathname).toBe(''); }); + + it('trackCustomEvent should call postWebAnalyticEvent', () => { + trackCustomEvent(MOCK_ANALYTICS_DATA); + + expect(postWebAnalyticEvent).toHaveBeenCalledWith(CUSTOM_EVENT_PAYLOAD); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.ts index 76e584fdf7a..7fbc8295789 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/WebAnalyticsUtils.ts @@ -17,8 +17,12 @@ import { setSession, } from '@analytics/session-utils'; import Analytics, { AnalyticsInstance } from 'analytics'; -import { WebPageData } from 'components/WebAnalytics/WebAnalytics.interface'; -import { postPageView } from 'rest/WebAnalyticsAPI'; +import { AnalyticsData } from 'components/WebAnalytics/WebAnalytics.interface'; +import { + CustomEvent, + CustomEventTypes, +} from 'generated/analytics/webAnalyticEventType/customEvent'; +import { postWebAnalyticEvent } from 'rest/WebAnalyticsAPI'; import { WebAnalyticEventData, WebAnalyticEventType, @@ -58,12 +62,30 @@ export const getPageLoadTime = (performance: Performance) => { ); }; +const handlePostAnalytic = async ( + webAnalyticEventData: WebAnalyticEventData +) => { + try { + /** + * extend the session expiry if user continues to interact + * Let say expiry is at "5:45:23 PM", and user spent some time and then + * interact with other page in 2 minutes so expiry time will be extended to "5:47:23 PM" + */ + setSession(30, {}, true); + + // collect the event data + await postWebAnalyticEvent(webAnalyticEventData); + } catch (_error) { + // silently ignore the error + } +}; + /** * track the page view if user id is available. * @param pageData PageData * @param userId string */ -export const trackPageView = async (pageData: WebPageData, userId: string) => { +export const trackPageView = (pageData: AnalyticsData, userId: string) => { // Get the current session const currentSession = getSession(); @@ -100,22 +122,40 @@ export const trackPageView = async (pageData: WebPageData, userId: string) => { timestamp, }; - try { - /** - * extend the session expiry if user continues to interact - * Let say expiry is at "5:45:23 PM", and user spent some time and then - * interact with other page in 2 minutes so expiry time will be extended to "5:47:23 PM" - */ - setSession(30, {}, true); - - // collect the page event - await postPageView(webAnalyticEventData); - } catch (_error) { - // handle page view error - } + handlePostAnalytic(webAnalyticEventData); } }; +export const trackCustomEvent = (eventData: AnalyticsData) => { + // Get the current session + const currentSession = getSession(); + + const { payload } = eventData; + + const { meta, event: eventValue } = payload; + const { location } = window; + + // timestamp for the current event + const timestamp = meta.ts; + + const customEventData: CustomEvent = { + url: location.pathname, + fullUrl: location.href, + hostname: location.hostname, + eventType: CustomEventTypes.Click, + sessionId: currentSession.id, + eventValue, + }; + + const webAnalyticEventData: WebAnalyticEventData = { + eventType: WebAnalyticEventType.CustomEvent, + eventData: customEventData, + timestamp, + }; + + handlePostAnalytic(webAnalyticEventData); +}; + /** * * @param userId string @@ -127,9 +167,12 @@ export const getAnalyticInstance = (userId: string): AnalyticsInstance => { plugins: [ { name: 'OM-Plugin', - page: (pageData: WebPageData) => { + page: (pageData: AnalyticsData) => { trackPageView(pageData, userId); }, + track: (trackingData: AnalyticsData) => { + trackCustomEvent(trackingData); + }, }, ], });