feat(analytics): add clickhouse (#11174)
This commit is contained in:
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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() }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user