feat(analytics): add clickhouse (#11174)

This commit is contained in:
Antoine Moreaux
2025-04-16 18:33:10 +02:00
committed by GitHub
parent b6901a49bf
commit 587281a541
66 changed files with 1858 additions and 244 deletions

View File

@ -1,8 +1,18 @@
import { gql } from '@apollo/client';
export const TRACK = gql`
mutation Track($action: String!, $payload: JSON!) {
track(action: $action, payload: $payload) {
export const TRACK_ANALYTICS = gql`
mutation TrackAnalytics(
$type: AnalyticsType!
$event: String
$name: String
$properties: JSON
) {
trackAnalytics(
type: $type
event: $event
name: $name
properties: $properties
) {
success
}
}

View File

@ -5,23 +5,76 @@ import { act, renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { useEventTracker } from '../useEventTracker';
import { ANALYTICS_COOKIE_NAME, useEventTracker } from '../useEventTracker';
import { AnalyticsType } from '~/generated/graphql';
// Mock document.cookie
Object.defineProperty(document, 'cookie', {
writable: true,
value: `${ANALYTICS_COOKIE_NAME}=exampleId`,
});
const mocks: MockedResponse[] = [
{
request: {
query: gql`
mutation Track($action: String!, $payload: JSON!) {
track(action: $action, payload: $payload) {
mutation TrackAnalytics(
$type: AnalyticsType!
$event: String
$name: String
$properties: JSON
) {
trackAnalytics(
type: $type
event: $event
name: $name
properties: $properties
) {
success
}
}
`,
variables: {
action: 'exampleType',
payload: {
type: AnalyticsType['TRACK'],
event: 'Example Event',
properties: {
foo: 'bar',
},
},
},
result: jest.fn(() => ({
data: {
track: {
success: true,
},
},
})),
},
{
request: {
query: gql`
mutation TrackAnalytics(
$type: AnalyticsType!
$event: String
$name: String
$properties: JSON
) {
trackAnalytics(
type: $type
event: $event
name: $name
properties: $properties
) {
success
}
}
`,
variables: {
type: AnalyticsType['PAGEVIEW'],
name: 'Example',
properties: {
sessionId: 'exampleId',
pathname: '',
pathname: '/example/path',
userAgent: '',
timeZone: '',
locale: '',
@ -50,24 +103,45 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
describe('useEventTracker', () => {
it('should make the call to track the event', async () => {
const eventType = 'exampleType';
const eventData = {
sessionId: 'exampleId',
pathname: '',
userAgent: '',
timeZone: '',
locale: '',
href: '',
referrer: '',
const payload = {
event: 'Example Event',
properties: {
foo: 'bar',
},
};
const { result } = renderHook(() => useEventTracker(), {
wrapper: Wrapper,
});
act(() => {
result.current(eventType, eventData);
result.current(AnalyticsType['TRACK'], payload);
});
await waitFor(() => {
expect(mocks[0].result).toHaveBeenCalled();
});
});
it('should make the call to track a pageview', async () => {
const payload = {
name: 'Example',
properties: {
sessionId: 'exampleId',
pathname: '/example/path',
userAgent: '',
timeZone: '',
locale: '',
href: '',
referrer: '',
},
};
const { result } = renderHook(() => useEventTracker(), {
wrapper: Wrapper,
});
act(() => {
result.current(AnalyticsType['PAGEVIEW'], payload);
});
await waitFor(() => {
expect(mocks[1].result).toHaveBeenCalled();
});
});
});

View File

@ -1,14 +1,11 @@
import { useCallback } from 'react';
import { v4 } from 'uuid';
import { useTrackMutation } from '~/generated/graphql';
export interface EventData {
pathname: string;
userAgent: string;
timeZone: string;
locale: string;
href: string;
referrer: string;
}
import {
AnalyticsType,
MutationTrackAnalyticsArgs,
useTrackAnalyticsMutation,
} from '~/generated/graphql';
export const ANALYTICS_COOKIE_NAME = 'analyticsCookie';
export const getSessionId = (): string => {
const cookie: { [key: string]: string } = {};
@ -28,16 +25,22 @@ export const setSessionId = (domain?: string): void => {
};
export const useEventTracker = () => {
const [createEventMutation] = useTrackMutation();
const [createEventMutation] = useTrackAnalyticsMutation();
return useCallback(
(eventAction: string, eventPayload: EventData) => {
(
type: AnalyticsType,
payload: Omit<MutationTrackAnalyticsArgs, 'type'>,
) => {
createEventMutation({
variables: {
action: eventAction,
payload: {
sessionId: getSessionId(),
...eventPayload,
type,
...payload,
properties: {
...payload.properties,
...(type === AnalyticsType['PAGEVIEW']
? { sessionId: getSessionId() }
: {}),
},
},
});

View File

@ -27,6 +27,8 @@ import { isDefined } from 'twenty-shared/utils';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
import { useInitializeQueryParamState } from '~/modules/app/hooks/useInitializeQueryParamState';
import { AnalyticsType } from '~/generated/graphql';
import { getPageTitleFromPath } from '~/utils/title-utils';
// TODO: break down into smaller functions and / or hooks
// - moved usePageChangeEffectNavigateLocation into dedicated hook
@ -174,13 +176,16 @@ export const PageChangeEffect = () => {
useEffect(() => {
setTimeout(() => {
setSessionId();
eventTracker('pageview', {
pathname: location.pathname,
locale: navigator.language,
userAgent: window.navigator.userAgent,
href: window.location.href,
referrer: document.referrer,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
eventTracker(AnalyticsType['PAGEVIEW'], {
name: getPageTitleFromPath(location.pathname),
properties: {
pathname: location.pathname,
locale: navigator.language,
userAgent: window.navigator.userAgent,
href: window.location.href,
referrer: document.referrer,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
});
}, 500);
}, [eventTracker, location.pathname]);