Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
efa0b5f
chore(analytics): remove tinybird
AMoreaux Mar 25, 2025
4a9af7e
chore(analytics): remove tinybird
AMoreaux Mar 25, 2025
131cf26
feat(server): add ClickHouse client dependency
AMoreaux Mar 25, 2025
29429e9
feat(database): add ClickHouse integration for analytics
AMoreaux Mar 25, 2025
b7c60d1
feat(analytics): integrate ClickHouse for event tracking
AMoreaux Mar 25, 2025
2566a7c
refactor(clickhouse): remove username and password support
AMoreaux Mar 26, 2025
100ee35
refactor(database): remove partitioning from MergeTree tables
AMoreaux Mar 26, 2025
8ac40b4
Merge remote-tracking branch 'origin/main' into feat/add-clickhouse
AMoreaux Apr 2, 2025
88551cb
fix(analytics): handle missing newline in service file
AMoreaux Apr 2, 2025
cf5b260
Merge remote-tracking branch 'origin/main' into feat/add-clickhouse
AMoreaux Apr 3, 2025
a1e954c
feat(analytics):add ci for clickhouse (#11339)
AMoreaux Apr 3, 2025
a5dc062
Merge branch 'main' into feat/add-clickhouse
AMoreaux Apr 3, 2025
492779c
chore(analytics): remove unused analytics service
AMoreaux Apr 3, 2025
c14bbeb
fix: remove insecure TLS configuration
AMoreaux Apr 3, 2025
99efde8
Merge branch 'main' into feat/add-clickhouse
AMoreaux Apr 3, 2025
c1638d2
feat(analytics): add custom domain events support and type-safe track…
AMoreaux Apr 4, 2025
f42610b
Merge remote-tracking branch 'origin/main' into feat/add-clickhouse
AMoreaux Apr 7, 2025
cd324b6
chore: remove twenty-analytics package references
AMoreaux Apr 7, 2025
0586f34
feat(analytics): enhance Clickhouse client and error handling
AMoreaux Apr 7, 2025
427b685
feat(analytics): add event buffering with scheduled flush
AMoreaux Apr 7, 2025
4f34d38
refactor(analytics): replace 'insert' with 'pushEvent' in service
AMoreaux Apr 7, 2025
d661199
test(analytics): add mock for ExceptionHandlerService
AMoreaux Apr 7, 2025
19123f0
refactor(analytics): remove ExceptionHandlerModule dependency
AMoreaux Apr 7, 2025
14d9a8d
test(clickhouse.service): add missing dependencies to test setup
AMoreaux Apr 7, 2025
0e7ccc6
refactor(analytics): remove CLICKHOUSE_DB and enhance flush logic
AMoreaux Apr 7, 2025
39684ce
test(analytics): remove redundant unit tests for ClickHouse service
AMoreaux Apr 7, 2025
a419210
refactor(telemetry): remove telemetry module and related code
AMoreaux Apr 7, 2025
be0110c
feat(telemetry): add telemetry module and enhance analytics
AMoreaux Apr 8, 2025
6b64173
Merge branch 'main' into feat/add-clickhouse
AMoreaux Apr 8, 2025
f99c0c8
feat(timeline): integrate analytics service in audit log job
AMoreaux Apr 8, 2025
2311712
feat(telemetry): integrate telemetry service into listener
AMoreaux Apr 8, 2025
840991b
feat(analytics): add integration tests for ClickHouse event and pagev…
AMoreaux Apr 8, 2025
f3dc050
refactor(analytics): improve Clickhouse integration and tests
AMoreaux Apr 8, 2025
c5ca786
chore(ci): add PostgreSQL service to CI workflow
AMoreaux Apr 8, 2025
f40a982
chore(ci): add Redis service to CI workflow
AMoreaux Apr 8, 2025
dc31324
feat(ci): integrate ClickHouse service and update workflows
AMoreaux Apr 8, 2025
a795c97
chore(ci): update workflows to include ClickHouse setup
AMoreaux Apr 8, 2025
74a7ec3
Merge branch 'main' into feat/add-clickhouse
AMoreaux Apr 8, 2025
2a637c7
refactor(ci): remove CI analytics workflow and update server setup
AMoreaux Apr 8, 2025
0660a6f
refactor(analytics): remove outdated integration tests
AMoreaux Apr 8, 2025
6986b9d
refactor(analytics): simplify payload handling and tests
AMoreaux Apr 9, 2025
1f18b41
feat(analytics): enhance integration test and add config
AMoreaux Apr 9, 2025
3d6c298
Merge remote-tracking branch 'origin/main' into feat/add-clickhouse
AMoreaux Apr 9, 2025
e78ca98
refactor(analytics): replace EnvironmentService with TwentyConfigService
AMoreaux Apr 9, 2025
b8556f7
refactor(messaging): simplify constructor formatting
AMoreaux Apr 9, 2025
f94a112
test(analytics): remove analytics service tests
AMoreaux Apr 9, 2025
364c805
fix(analytics): use env variable for ClickHouse URL
AMoreaux Apr 9, 2025
60c10eb
refactor(analytics): remove scheduled buffer flush logic
AMoreaux Apr 10, 2025
4dc7059
chore(ci): update ClickHouse credentials and environment config
AMoreaux Apr 10, 2025
d0fae4d
fix(ci): add missing ClickHouse password to environment variables
AMoreaux Apr 10, 2025
6cdc66f
fix(ci): add ClickHouse password and update example config
AMoreaux Apr 10, 2025
c8c416a
chore(ci): update ClickHouse environment configuration
AMoreaux Apr 10, 2025
3842419
fix(ci): update health-check command for ClickHouse container
AMoreaux Apr 10, 2025
d095890
fix(ci): update ClickHouse health check command
AMoreaux Apr 10, 2025
912e43b
refactor(analytics): streamline event tracking architecture
AMoreaux Apr 10, 2025
f3c18c7
refactor(analytics): streamline event tracking architecture
AMoreaux Apr 14, 2025
0f6bc34
Merge remote-tracking branch 'origin/main' into feat/add-clickhouse
AMoreaux Apr 14, 2025
f43d3b0
feat(analytics): improve input validation and tracking logic
AMoreaux Apr 14, 2025
e036afe
feat(analytics): refactor tracking to use strongly-typed enums
AMoreaux Apr 14, 2025
164399e
refactor(analytics): format imports and fix payload syntax
AMoreaux Apr 14, 2025
36e29ed
feat(analytics): enhance pageview tracking with properties
AMoreaux Apr 14, 2025
e1656be
feat(analytics): enhance error handling for analytics service
AMoreaux Apr 14, 2025
94e886f
Merge branch 'main' into feat/add-clickhouse
AMoreaux Apr 14, 2025
ec2da40
fix(analytics): conditionally include sessionId for pageviews
AMoreaux Apr 14, 2025
3cc597c
fix(telemetry): add missing options parameter to track method
AMoreaux Apr 14, 2025
30329b3
Merge remote-tracking branch 'origin/main' into feat/add-clickhouse
AMoreaux Apr 14, 2025
404bf6b
Merge remote-tracking branch 'origin/main' into feat/add-clickhouse
AMoreaux Apr 15, 2025
7191409
Merge branch 'main' into feat/add-clickhouse
AMoreaux Apr 15, 2025
36214f4
refactor(user-workspace): update constant reference for signup event
AMoreaux Apr 15, 2025
4b792a6
refactor(analytics): update event registry to use ZodObject
AMoreaux Apr 15, 2025
14fccd6
refactor(analytics): update type from ZodObject to ZodSchema
AMoreaux Apr 15, 2025
eedb63f
Merge remote-tracking branch 'origin/main' into feat/add-clickhouse
AMoreaux Apr 15, 2025
2a13925
feat(analytics): introduce version 2 of tracking API
AMoreaux Apr 15, 2025
c724c16
refactor(analytics): prefix unused parameters with underscores
AMoreaux Apr 15, 2025
3bc7cf1
refactor(analytics): update test to use trackV2 method
AMoreaux Apr 16, 2025
77d8851
feat(analytics): add specific object record event types
AMoreaux Apr 16, 2025
2fe4f2f
refactor(test): remove debug log from analytics test
AMoreaux Apr 16, 2025
1e5c5c4
refactor(analytics): remove outdated comment in test
AMoreaux Apr 16, 2025
e68044b
refactor(analytics): enhance schema and streamline telemetry logic
AMoreaux Apr 16, 2025
a9f544d
Merge remote-tracking branch 'origin/main' into feat/add-clickhouse
AMoreaux Apr 16, 2025
6092250
refactor(timeline): remove debug logging from audit log job
AMoreaux Apr 16, 2025
3c18ce0
refactor(analytics): rename `TrackV2` to `TrackAnalytics`
AMoreaux Apr 16, 2025
a7bc801
refactor(analytics): format GraphQL query and hook parameters
AMoreaux Apr 16, 2025
b14ff51
refactor(analytics/workspace): improve typings and validation
AMoreaux Apr 16, 2025
aa5197b
Merge branch 'main' into feat/add-clickhouse
AMoreaux Apr 16, 2025
c32d2dc
fix(analytics): handle null properties in analytics input
AMoreaux Apr 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .github/workflows/ci-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,33 @@ jobs:
image: redis
ports:
- 6379:6379
clickhouse:
image: clickhouse/clickhouse-server:latest
env:
CLICKHOUSE_PASSWORD: clickhousePassword
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
ports:
- 8123:8123
- 9000:9000
options: >-
--health-cmd "clickhouse-client --host=localhost --port=9000 --user=default --password=clickhousePassword --query='SELECT 1'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
NODE_ENV: test
ANALYTICS_ENABLED: true
CLICKHOUSE_URL: "http://default:clickhousePassword@localhost:8123/twenty"
CLICKHOUSE_PASSWORD: clickhousePassword
steps:
- name: Fetch custom Github Actions and base branch history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dependencies
uses: ./.github/workflows/actions/yarn-install
- name: Update .env.test for billing
- name: Update .env.test for integrations tests
run: |
echo "IS_BILLING_ENABLED=true" >> .env.test
echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
Expand All @@ -198,6 +214,10 @@ jobs:
- name: Server / Create Test DB
run: |
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
- name: Run ClickHouse migrations
run: npx nx clickhouse:migrate twenty-server
- name: Run ClickHouse seeds
run: npx nx clickhouse:seed twenty-server
- name: Server / Run Integration Tests
uses: ./.github/workflows/actions/nx-affected
with:
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ postgres-on-docker:
-c "CREATE DATABASE \"test\" WITH OWNER postgres;"

redis-on-docker:
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest

clickhouse-on-docker:
docker run -d --name twenty_clickhouse -p 8123:8123 -p 9000:9000 -e CLICKHOUSE_PASSWORD=devPassword clickhouse/clickhouse-server:latest
2 changes: 0 additions & 2 deletions packages/twenty-front/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const path = require('path');

module.exports = {
extends: ['../../.eslintrc.react.cjs'],
ignorePatterns: [
Expand Down
31 changes: 28 additions & 3 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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']>;
Expand Down
60 changes: 60 additions & 0 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -899,6 +904,7 @@ export type Mutation = {
submitFormStep: Scalars['Boolean'];
switchToYearlyInterval: BillingUpdateOutput;
track: Analytics;
trackAnalytics: Analytics;
updateLabPublicFeatureFlag: FeatureFlagDto;
updateOneField: Field;
updateOneObject: Object;
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
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: {
action: 'exampleType',
payload: {
type: AnalyticsType['PAGEVIEW'],
name: 'Example',
properties: {
sessionId: 'exampleId',
pathname: '',
pathname: '/example/path',
userAgent: '',
timeZone: '',
locale: '',
Expand Down Expand Up @@ -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();
});
});
});
Loading
Loading