feat(analytics): add clickhouse (#11174)
This commit is contained in:
@ -63,6 +63,11 @@ export type Analytics = {
|
||||
success: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export enum AnalyticsType {
|
||||
PAGEVIEW = 'PAGEVIEW',
|
||||
TRACK = 'TRACK'
|
||||
}
|
||||
|
||||
export type ApiConfig = {
|
||||
__typename?: 'ApiConfig';
|
||||
mutationMaximumAffectedRecords: Scalars['Float']['output'];
|
||||
@ -155,7 +160,8 @@ export type BillingEndTrialPeriodOutput = {
|
||||
|
||||
export type BillingMeteredProductUsageOutput = {
|
||||
__typename?: 'BillingMeteredProductUsageOutput';
|
||||
includedFreeQuantity: Scalars['Float']['output'];
|
||||
freeTierQuantity: Scalars['Float']['output'];
|
||||
freeTrialQuantity: Scalars['Float']['output'];
|
||||
periodEnd: Scalars['DateTime']['output'];
|
||||
periodStart: Scalars['DateTime']['output'];
|
||||
productKey: BillingProductKey;
|
||||
@ -474,6 +480,10 @@ export type CreateServerlessFunctionInput = {
|
||||
};
|
||||
|
||||
export type CreateWorkflowVersionStepInput = {
|
||||
/** Next step ID */
|
||||
nextStepId?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Parent step ID */
|
||||
parentStepId?: InputMaybe<Scalars['String']['input']>;
|
||||
/** New step type */
|
||||
stepType: Scalars['String']['input'];
|
||||
/** Workflow version ID */
|
||||
@ -598,6 +608,12 @@ export type FeatureFlag = {
|
||||
workspaceId: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type FeatureFlagDto = {
|
||||
__typename?: 'FeatureFlagDTO';
|
||||
key: FeatureFlagKey;
|
||||
value: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export enum FeatureFlagKey {
|
||||
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
||||
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
|
||||
@ -970,8 +986,9 @@ export type Mutation = {
|
||||
syncRemoteTable: RemoteTable;
|
||||
syncRemoteTableSchemaChanges: RemoteTable;
|
||||
track: Analytics;
|
||||
trackAnalytics: Analytics;
|
||||
unsyncRemoteTable: RemoteTable;
|
||||
updateLabPublicFeatureFlag: FeatureFlag;
|
||||
updateLabPublicFeatureFlag: FeatureFlagDto;
|
||||
updateOneField: Field;
|
||||
updateOneObject: Object;
|
||||
updateOneRemoteServer: RemoteServer;
|
||||
@ -1252,6 +1269,14 @@ export type MutationTrackArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationTrackAnalyticsArgs = {
|
||||
event?: InputMaybe<Scalars['String']['input']>;
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
properties?: InputMaybe<Scalars['JSON']['input']>;
|
||||
type: AnalyticsType;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUnsyncRemoteTableArgs = {
|
||||
input: RemoteTableInput;
|
||||
};
|
||||
@ -2462,7 +2487,7 @@ export type Workspace = {
|
||||
defaultRole?: Maybe<Role>;
|
||||
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
featureFlags?: Maybe<Array<FeatureFlag>>;
|
||||
featureFlags?: Maybe<Array<FeatureFlagDto>>;
|
||||
hasValidEnterpriseKey: Scalars['Boolean']['output'];
|
||||
id: Scalars['UUID']['output'];
|
||||
inviteHash?: Maybe<Scalars['String']['output']>;
|
||||
|
||||
@ -55,6 +55,11 @@ export type Analytics = {
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export enum AnalyticsType {
|
||||
PAGEVIEW = 'PAGEVIEW',
|
||||
TRACK = 'TRACK'
|
||||
}
|
||||
|
||||
export type ApiConfig = {
|
||||
__typename?: 'ApiConfig';
|
||||
mutationMaximumAffectedRecords: Scalars['Float'];
|
||||
@ -899,6 +904,7 @@ export type Mutation = {
|
||||
submitFormStep: Scalars['Boolean'];
|
||||
switchToYearlyInterval: BillingUpdateOutput;
|
||||
track: Analytics;
|
||||
trackAnalytics: Analytics;
|
||||
updateLabPublicFeatureFlag: FeatureFlagDto;
|
||||
updateOneField: Field;
|
||||
updateOneObject: Object;
|
||||
@ -1139,6 +1145,14 @@ export type MutationTrackArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationTrackAnalyticsArgs = {
|
||||
event?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
properties?: InputMaybe<Scalars['JSON']>;
|
||||
type: AnalyticsType;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateLabPublicFeatureFlagArgs = {
|
||||
input: UpdateLabPublicFeatureFlagInput;
|
||||
};
|
||||
@ -2403,6 +2417,16 @@ export type GetTimelineThreadsFromPersonIdQueryVariables = Exact<{
|
||||
|
||||
export type GetTimelineThreadsFromPersonIdQuery = { __typename?: 'Query', getTimelineThreadsFromPersonId: { __typename?: 'TimelineThreadsWithTotal', totalNumberOfThreads: number, timelineThreads: Array<{ __typename?: 'TimelineThread', id: any, read: boolean, visibility: MessageChannelVisibility, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
|
||||
|
||||
export type TrackAnalyticsMutationVariables = Exact<{
|
||||
type: AnalyticsType;
|
||||
event?: InputMaybe<Scalars['String']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
properties?: InputMaybe<Scalars['JSON']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TrackAnalyticsMutation = { __typename?: 'Mutation', trackAnalytics: { __typename?: 'Analytics', success: boolean } };
|
||||
|
||||
export type TrackMutationVariables = Exact<{
|
||||
action: Scalars['String'];
|
||||
payload: Scalars['JSON'];
|
||||
@ -3326,6 +3350,42 @@ export function useGetTimelineThreadsFromPersonIdLazyQuery(baseOptions?: Apollo.
|
||||
export type GetTimelineThreadsFromPersonIdQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromPersonIdQuery>;
|
||||
export type GetTimelineThreadsFromPersonIdLazyQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromPersonIdLazyQuery>;
|
||||
export type GetTimelineThreadsFromPersonIdQueryResult = Apollo.QueryResult<GetTimelineThreadsFromPersonIdQuery, GetTimelineThreadsFromPersonIdQueryVariables>;
|
||||
export const TrackAnalyticsDocument = gql`
|
||||
mutation TrackAnalytics($type: AnalyticsType!, $event: String, $name: String, $properties: JSON) {
|
||||
trackAnalytics(type: $type, event: $event, name: $name, properties: $properties) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type TrackAnalyticsMutationFn = Apollo.MutationFunction<TrackAnalyticsMutation, TrackAnalyticsMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useTrackAnalyticsMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useTrackAnalyticsMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useTrackAnalyticsMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [trackAnalyticsMutation, { data, loading, error }] = useTrackAnalyticsMutation({
|
||||
* variables: {
|
||||
* type: // value for 'type'
|
||||
* event: // value for 'event'
|
||||
* name: // value for 'name'
|
||||
* properties: // value for 'properties'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTrackAnalyticsMutation(baseOptions?: Apollo.MutationHookOptions<TrackAnalyticsMutation, TrackAnalyticsMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<TrackAnalyticsMutation, TrackAnalyticsMutationVariables>(TrackAnalyticsDocument, options);
|
||||
}
|
||||
export type TrackAnalyticsMutationHookResult = ReturnType<typeof useTrackAnalyticsMutation>;
|
||||
export type TrackAnalyticsMutationResult = Apollo.MutationResult<TrackAnalyticsMutation>;
|
||||
export type TrackAnalyticsMutationOptions = Apollo.BaseMutationOptions<TrackAnalyticsMutation, TrackAnalyticsMutationVariables>;
|
||||
export const TrackDocument = gql`
|
||||
mutation Track($action: String!, $payload: JSON!) {
|
||||
track(action: $action, payload: $payload) {
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { graphql, GraphQLQuery, http, HttpResponse } from 'msw';
|
||||
|
||||
import { TRACK } from '@/analytics/graphql/queries/track';
|
||||
import { TRACK_ANALYTICS } from '@/analytics/graphql/queries/track';
|
||||
import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig';
|
||||
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
@ -110,10 +110,10 @@ export const graphqlMocks = {
|
||||
});
|
||||
},
|
||||
),
|
||||
graphql.mutation(getOperationName(TRACK) ?? '', () => {
|
||||
graphql.mutation(getOperationName(TRACK_ANALYTICS) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
track: { success: 1, __typename: 'TRACK' },
|
||||
track: { success: 1, __typename: 'TRACK_ANALYTICS' },
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user