diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx index bf083a157e..05c99cc89d 100644 --- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx @@ -4,7 +4,10 @@ import { useRecoilValue } from 'recoil'; import { IconCheckbox } from 'twenty-ui'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; -import { useEventTracker } from '@/analytics/hooks/useEventTracker'; +import { + setSessionId, + useEventTracker, +} from '@/analytics/hooks/useEventTracker'; import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken'; import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; @@ -163,10 +166,14 @@ export const PageChangeEffect = () => { useEffect(() => { setTimeout(() => { + setSessionId(); eventTracker('pageview', { - location: { - pathname: location.pathname, - }, + 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]); diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index ce122e418c..09fac64ea5 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -165,7 +165,6 @@ export type ClientConfig = { signInPrefilled: Scalars['Boolean']['output']; signUpDisabled: Scalars['Boolean']['output']; support: Support; - telemetry: Telemetry; }; export type CreateAppTokenInput = { @@ -1171,11 +1170,6 @@ export type Support = { supportFrontChatId?: Maybe; }; -export type Telemetry = { - __typename?: 'Telemetry'; - enabled: Scalars['Boolean']['output']; -}; - export type TimelineCalendarEvent = { __typename?: 'TimelineCalendarEvent'; conferenceLink: LinksMetadata; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 15c8a28790..02d20ed6fd 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; +import { gql } from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -158,7 +158,6 @@ export type ClientConfig = { signInPrefilled: Scalars['Boolean']; signUpDisabled: Scalars['Boolean']; support: Support; - telemetry: Telemetry; }; export type CreateServerlessFunctionFromFileInput = { @@ -524,6 +523,7 @@ export type MutationSignUpArgs = { export type MutationTrackArgs = { data: Scalars['JSON']; + sessionId: Scalars['String']; type: Scalars['String']; }; @@ -919,11 +919,6 @@ export type Support = { supportFrontChatId?: Maybe; }; -export type Telemetry = { - __typename?: 'Telemetry'; - enabled: Scalars['Boolean']; -}; - export type TimelineCalendarEvent = { __typename?: 'TimelineCalendarEvent'; conferenceLink: LinksMetadata; @@ -1354,8 +1349,8 @@ 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 TrackMutationVariables = Exact<{ - type: Scalars['String']; - data: Scalars['JSON']; + action: Scalars['String']; + payload: Scalars['JSON']; }>; @@ -1511,7 +1506,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; @@ -1948,8 +1943,8 @@ export type GetTimelineThreadsFromPersonIdQueryHookResult = ReturnType; export type GetTimelineThreadsFromPersonIdQueryResult = Apollo.QueryResult; export const TrackDocument = gql` - mutation Track($type: String!, $data: JSON!) { - track(type: $type, data: $data) { + mutation Track($action: String!, $payload: JSON!) { + track(action: $action, payload: $payload) { success } } @@ -1969,8 +1964,8 @@ export type TrackMutationFn = Apollo.MutationFunction ({ @@ -43,7 +51,15 @@ const Wrapper = ({ children }: { children: ReactNode }) => ( describe('useEventTracker', () => { it('should make the call to track the event', async () => { const eventType = 'exampleType'; - const eventData = { location: { pathname: '/examplePath' } }; + const eventData = { + sessionId: 'exampleId', + pathname: '', + userAgent: '', + timeZone: '', + locale: '', + href: '', + referrer: '', + }; const { result } = renderHook(() => useEventTracker(), { wrapper: Wrapper, }); diff --git a/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts b/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts index 88d1d65674..faaa3ea5b6 100644 --- a/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts +++ b/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts @@ -1,32 +1,46 @@ import { useCallback } from 'react'; -import { useRecoilValue } from 'recoil'; - -import { telemetryState } from '@/client-config/states/telemetryState'; import { useTrackMutation } from '~/generated/graphql'; - -interface EventLocation { - pathname: string; -} - export interface EventData { - location: EventLocation; + pathname: string; + userAgent: string; + timeZone: string; + locale: string; + href: string; + referrer: string; } +export const ANALYTICS_COOKIE_NAME = 'analyticsCookie'; +export const getSessionId = (): string => { + const cookie: { [key: string]: string } = {}; + document.cookie.split(';').forEach((el) => { + const [key, value] = el.split('='); + cookie[key.trim()] = value; + }); + return cookie[ANALYTICS_COOKIE_NAME]; +}; + +export const setSessionId = (domain?: string): void => { + const sessionId = getSessionId() || crypto.randomUUID(); + const baseCookie = `${ANALYTICS_COOKIE_NAME}=${sessionId}; Max-Age=1800; path=/; secure`; + const cookie = domain ? baseCookie + `; domain=${domain}` : baseCookie; + + document.cookie = cookie; +}; export const useEventTracker = () => { - const telemetry = useRecoilValue(telemetryState); const [createEventMutation] = useTrackMutation(); return useCallback( - (eventType: string, eventData: EventData) => { - if (telemetry.enabled) { - createEventMutation({ - variables: { - type: eventType, - data: eventData, + (eventAction: string, eventPayload: EventData) => { + createEventMutation({ + variables: { + action: eventAction, + payload: { + sessionId: getSessionId(), + ...eventPayload, }, - }); - } + }, + }); }, - [createEventMutation, telemetry], + [createEventMutation], ); }; diff --git a/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx b/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx index 59f99306a5..5e19a8309c 100644 --- a/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx +++ b/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx @@ -1,7 +1,7 @@ -import { MemoryRouter, useLocation } from 'react-router-dom'; import { ApolloError, gql } from '@apollo/client'; import { act, renderHook } from '@testing-library/react'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; +import { MemoryRouter, useLocation } from 'react-router-dom'; import { RecoilRoot } from 'recoil'; import { useApolloFactory } from '../useApolloFactory'; @@ -77,8 +77,8 @@ describe('useApolloFactory', () => { await act(async () => { await result.current.factory.mutate({ mutation: gql` - mutation Track($type: String!, $data: JSON!) { - track(type: $type, data: $data) { + mutation Track($type: String!, $sessionId: String!, $data: JSON!) { + track(type: $type, sessionId: $sessionId, data: $data) { success } } diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index d0ba375124..9136b83fcd 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -41,8 +41,8 @@ const makeRequest = async () => { await client.mutate({ mutation: gql` - mutation Track($type: String!, $data: JSON!) { - track(type: $type, data: $data) { + mutation Track($type: String!, $sessionId: String!, $data: JSON!) { + track(type: $type, sessionId: $sessionId, data: $data) { success } } diff --git a/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx index ac52204f4c..60e4025a81 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx +++ b/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx @@ -1,8 +1,8 @@ -import { ReactNode } from 'react'; import { useApolloClient } from '@apollo/client'; import { MockedProvider } from '@apollo/client/testing'; import { expect } from '@storybook/test'; import { act, renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; import { RecoilRoot, useRecoilValue } from 'recoil'; import { iconsState } from 'twenty-ui'; @@ -12,7 +12,6 @@ import { billingState } from '@/client-config/states/billingState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { supportChatState } from '@/client-config/states/supportChatState'; -import { telemetryState } from '@/client-config/states/telemetryState'; import { email, mocks, password, results, token } from '../__mocks__/useAuth'; @@ -81,7 +80,6 @@ describe('useAuth', () => { const billing = useRecoilValue(billingState); const isSignInPrefilled = useRecoilValue(isSignInPrefilledState); const supportChat = useRecoilValue(supportChatState); - const telemetry = useRecoilValue(telemetryState); const isDebugMode = useRecoilValue(isDebugModeState); return { ...useAuth(), @@ -92,7 +90,6 @@ describe('useAuth', () => { billing, isSignInPrefilled, supportChat, - telemetry, isDebugMode, }, }; @@ -126,9 +123,6 @@ describe('useAuth', () => { supportDriver: 'none', supportFrontChatId: null, }); - expect(state.telemetry).toEqual({ - enabled: true, - }); expect(state.isDebugMode).toBe(false); }); diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 677932161d..7a7de0807f 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -21,7 +21,6 @@ import { isClientConfigLoadedState } from '@/client-config/states/isClientConfig import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { supportChatState } from '@/client-config/states/supportChatState'; -import { telemetryState } from '@/client-config/states/telemetryState'; import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { @@ -224,7 +223,6 @@ export const useAuth = () => { .getLoadable(isSignInPrefilledState) .getValue(); const supportChat = snapshot.getLoadable(supportChatState).getValue(); - const telemetry = snapshot.getLoadable(telemetryState).getValue(); const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue(); const captchaProvider = snapshot .getLoadable(captchaProviderState) @@ -242,7 +240,6 @@ export const useAuth = () => { set(billingState, billing); set(isSignInPrefilledState, isSignInPrefilled); set(supportChatState, supportChat); - set(telemetryState, telemetry); set(isDebugModeState, isDebugMode); set(captchaProviderState, captchaProvider); set(isClientConfigLoadedState, isClientConfigLoaded); diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index 8bec4cc7db..9eccbeb98e 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -12,7 +12,6 @@ import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilled import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { supportChatState } from '@/client-config/states/supportChatState'; -import { telemetryState } from '@/client-config/states/telemetryState'; import { useGetClientConfigQuery } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; @@ -24,7 +23,6 @@ export const ClientConfigProviderEffect = () => { const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState); const setBilling = useSetRecoilState(billingState); - const setTelemetry = useSetRecoilState(telemetryState); const setSupportChat = useSetRecoilState(supportChatState); const setSentryConfig = useSetRecoilState(sentryConfigState); @@ -56,7 +54,6 @@ export const ClientConfigProviderEffect = () => { setIsSignUpDisabled(data?.clientConfig.signUpDisabled); setBilling(data?.clientConfig.billing); - setTelemetry(data?.clientConfig.telemetry); setSupportChat(data?.clientConfig.support); setSentryConfig({ @@ -79,7 +76,6 @@ export const ClientConfigProviderEffect = () => { setIsDebugMode, setIsSignInPrefilled, setIsSignUpDisabled, - setTelemetry, setSupportChat, setBilling, setSentryConfig, diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index 3143bbc5f6..e702acefa4 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -16,9 +16,6 @@ export const GET_CLIENT_CONFIG = gql` signInPrefilled signUpDisabled debugMode - telemetry { - enabled - } support { supportDriver supportFrontChatId diff --git a/packages/twenty-front/src/modules/client-config/states/telemetryState.ts b/packages/twenty-front/src/modules/client-config/states/telemetryState.ts deleted file mode 100644 index f074ad218d..0000000000 --- a/packages/twenty-front/src/modules/client-config/states/telemetryState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createState } from 'twenty-ui'; - -import { Telemetry } from '~/generated/graphql'; - -export const telemetryState = createState({ - key: 'telemetryState', - defaultValue: { enabled: true }, -}); diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index 656dcbb80b..1ed65869a7 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -13,10 +13,6 @@ export const mockedClientConfig: ClientConfig = { microsoft: false, __typename: 'AuthProviders', }, - telemetry: { - enabled: false, - __typename: 'Telemetry', - }, support: { supportDriver: 'front', supportFrontChatId: null, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts index 2afa537a68..f627caf47a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener.ts @@ -2,15 +2,15 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; +import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type'; @Injectable() export class TelemetryListener { constructor( private readonly analyticsService: AnalyticsService, - private readonly environmentService: EnvironmentService, + private readonly telemetryService: TelemetryService, ) {} @OnEvent('*.created') @@ -21,16 +21,11 @@ export class TelemetryListener { payload.events.map((eventPayload) => this.analyticsService.create( { - type: 'track', - data: { - eventName: payload.name, - }, + action: payload.name, + payload: {}, }, eventPayload.userId, payload.workspaceId, - '', // voluntarily not retrieving this - '', // to avoid slowing down - this.environmentService.get('SERVER_URL'), ), ), ); @@ -41,21 +36,29 @@ export class TelemetryListener { payload: WorkspaceEventBatch>, ) { await Promise.all( - payload.events.map((eventPayload) => + payload.events.map(async (eventPayload) => { this.analyticsService.create( { - type: 'track', - data: { - eventName: 'user.signup', + action: 'user.signup', + payload: {}, + }, + eventPayload.userId, + payload.workspaceId, + ); + + this.telemetryService.create( + { + action: 'user.signup', + payload: { + payload, + userId: undefined, + workspaceId: undefined, }, }, eventPayload.userId, payload.workspaceId, - '', - '', - this.environmentService.get('SERVER_URL'), - ), - ), + ); + }), ); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index 5d1771aad8..1e166806be 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -12,6 +12,7 @@ import { DuplicateModule } from 'src/engine/core-modules/duplicate/duplicate.mod import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; +import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -29,6 +30,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]), TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), AnalyticsModule, + TelemetryModule, DuplicateModule, FileModule, FeatureFlagModule, diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts index 7ce66bafdd..2b4c8705d6 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts @@ -1,14 +1,16 @@ -import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; -import { AnalyticsService } from './analytics.service'; import { AnalyticsResolver } from './analytics.resolver'; +import { AnalyticsService } from './analytics.service'; + +const TINYBIRD_BASE_URL = 'https://api.eu-central-1.aws.tinybird.co/v0'; @Module({ providers: [AnalyticsResolver, AnalyticsService], imports: [ HttpModule.register({ - baseURL: 'https://t.twenty.com/api/v1/s2s', + baseURL: TINYBIRD_BASE_URL, }), ], exports: [AnalyticsService], diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts index 06cd9aabed..f64d108fcd 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts @@ -31,9 +31,6 @@ export class AnalyticsResolver { createAnalyticsInput, user?.id, workspace?.id, - workspace?.displayName, - workspace?.domainName, - this.environmentService.get('SERVER_URL') ?? request.hostname, ); } } diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts index 085f443783..3e8ca56d22 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts @@ -1,16 +1,19 @@ -import { Injectable, Logger } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; +import { Injectable, Logger } from '@nestjs/common'; + +import { AxiosRequestConfig } from 'axios'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; type CreateEventInput = { - type: string; - data: object; + action: string; + payload: object; }; @Injectable() export class AnalyticsService { private readonly logger = new Logger(AnalyticsService.name); + private readonly datasource = 'event'; constructor( private readonly environmentService: EnvironmentService, @@ -21,30 +24,42 @@ export class AnalyticsService { createEventInput: CreateEventInput, userId: string | null | undefined, workspaceId: string | null | undefined, - workspaceDisplayName: string | undefined, - workspaceDomainName: string | undefined, - hostName: string | undefined, ) { - if (!this.environmentService.get('TELEMETRY_ENABLED')) { + if (this.environmentService.get('ANALYTICS_ENABLED')) { return { success: true }; } const data = { - type: createEventInput.type, - data: { - hostname: hostName, - userUUID: userId, - workspaceUUID: workspaceId, - workspaceDisplayName: workspaceDisplayName, - workspaceDomainName: workspaceDomainName, - ...createEventInput.data, + action: createEventInput.action, + timestamp: new Date().toISOString(), + version: '1', + payload: { + userId: userId, + workspaceId: workspaceId, + ...createEventInput.payload, + }, + }; + + const config: AxiosRequestConfig = { + headers: { + Authorization: + 'Bearer ' + this.environmentService.get('TINYBIRD_TOKEN'), }, }; try { - await this.httpService.axiosRef.post('/v1', data); - } catch { - this.logger.error('Failed to send analytics event'); + await this.httpService.axiosRef.post( + `/events?name=${this.datasource}`, + data, + config, + ); + } catch (error) { + this.logger.error('Error occurred:', error); + if (error.response) { + this.logger.error( + `Error response body: ${JSON.stringify(error.response.data)}`, + ); + } return { success: false }; } diff --git a/packages/twenty-server/src/engine/core-modules/analytics/dtos/create-analytics.input.ts b/packages/twenty-server/src/engine/core-modules/analytics/dtos/create-analytics.input.ts index a870673fe1..5e887dca05 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/dtos/create-analytics.input.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/dtos/create-analytics.input.ts @@ -1,16 +1,16 @@ import { ArgsType, Field } from '@nestjs/graphql'; +import { IsNotEmpty, IsObject, IsString } from 'class-validator'; import graphqlTypeJson from 'graphql-type-json'; -import { IsNotEmpty, IsString, IsObject } from 'class-validator'; @ArgsType() export class CreateAnalyticsInput { @Field({ description: 'Type of the event' }) @IsNotEmpty() @IsString() - type: string; + action: string; - @Field(() => graphqlTypeJson, { description: 'Event data in JSON format' }) + @Field(() => graphqlTypeJson, { description: 'Event payload in JSON format' }) @IsObject() - data: JSON; + payload: JSON; } diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index eefb2509be..12cf5c3164 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -76,9 +76,6 @@ export class ClientConfig { @Field(() => AuthProviders, { nullable: false }) authProviders: AuthProviders; - @Field(() => Telemetry, { nullable: false }) - telemetry: Telemetry; - @Field(() => Billing, { nullable: false }) billing: Billing; diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index 54844671bb..3615066a43 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -1,4 +1,4 @@ -import { Resolver, Query } from '@nestjs/graphql'; +import { Query, Resolver } from '@nestjs/graphql'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; @@ -17,9 +17,6 @@ export class ClientConfigResolver { password: this.environmentService.get('AUTH_PASSWORD_ENABLED'), microsoft: this.environmentService.get('AUTH_MICROSOFT_ENABLED'), }, - telemetry: { - enabled: this.environmentService.get('TELEMETRY_ENABLED'), - }, billing: { isBillingEnabled: this.environmentService.get('IS_BILLING_ENABLED'), billingUrl: this.environmentService.get('BILLING_PLAN_REQUIRED_LINK'), diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index a78b636e2a..2e2df06c4d 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -7,43 +7,44 @@ import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-qu import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; +import { CacheStorageModule } from 'src/engine/core-modules/cache-storage/cache-storage.module'; import { TimelineCalendarEventModule } from 'src/engine/core-modules/calendar/timeline-calendar-event.module'; -import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { HealthModule } from 'src/engine/core-modules/health/health.module'; -import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timeline-messaging.module'; -import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module'; -import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module'; -import { UserModule } from 'src/engine/core-modules/user/user.module'; -import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workflow-trigger-api.module'; -import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; -import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; -import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module'; -import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; -import { fileStorageModuleFactory } from 'src/engine/core-modules/file-storage/file-storage.module-factory'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { LoggerModule } from 'src/engine/core-modules/logger/logger.module'; -import { loggerModuleFactory } from 'src/engine/core-modules/logger/logger.module-factory'; -import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module'; -import { messageQueueModuleFactory } from 'src/engine/core-modules/message-queue/message-queue.module-factory'; -import { ExceptionHandlerModule } from 'src/engine/core-modules/exception-handler/exception-handler.module'; -import { exceptionHandlerModuleFactory } from 'src/engine/core-modules/exception-handler/exception-handler.module-factory'; -import { EmailModule } from 'src/engine/core-modules/email/email.module'; -import { emailModuleFactory } from 'src/engine/core-modules/email/email.module-factory'; import { CaptchaModule } from 'src/engine/core-modules/captcha/captcha.module'; import { captchaModuleFactory } from 'src/engine/core-modules/captcha/captcha.module-factory'; -import { CacheStorageModule } from 'src/engine/core-modules/cache-storage/cache-storage.module'; +import { EmailModule } from 'src/engine/core-modules/email/email.module'; +import { emailModuleFactory } from 'src/engine/core-modules/email/email.module-factory'; +import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { ExceptionHandlerModule } from 'src/engine/core-modules/exception-handler/exception-handler.module'; +import { exceptionHandlerModuleFactory } from 'src/engine/core-modules/exception-handler/exception-handler.module-factory'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module'; +import { fileStorageModuleFactory } from 'src/engine/core-modules/file-storage/file-storage.module-factory'; +import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { HealthModule } from 'src/engine/core-modules/health/health.module'; import { LLMChatModelModule } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.module'; import { llmChatModelModuleFactory } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.module-factory'; import { LLMTracingModule } from 'src/engine/core-modules/llm-tracing/llm-tracing.module'; import { llmTracingModuleFactory } from 'src/engine/core-modules/llm-tracing/llm-tracing.module-factory'; -import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module'; +import { LoggerModule } from 'src/engine/core-modules/logger/logger.module'; +import { loggerModuleFactory } from 'src/engine/core-modules/logger/logger.module-factory'; +import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module'; +import { messageQueueModuleFactory } from 'src/engine/core-modules/message-queue/message-queue.module-factory'; +import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timeline-messaging.module'; +import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module'; +import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module'; import { serverlessModuleFactory } from 'src/engine/core-modules/serverless/serverless-module.factory'; -import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module'; +import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; +import { UserModule } from 'src/engine/core-modules/user/user.module'; +import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workflow-trigger-api.module'; import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; +import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; +import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; -import { FileModule } from './file/file.module'; -import { ClientConfigModule } from './client-config/client-config.module'; import { AnalyticsModule } from './analytics/analytics.module'; +import { ClientConfigModule } from './client-config/client-config.module'; +import { FileModule } from './file/file.module'; @Module({ imports: [ @@ -66,6 +67,7 @@ import { AnalyticsModule } from './analytics/analytics.module'; WorkflowTriggerApiModule, WorkspaceEventEmitterModule, ActorModule, + TelemetryModule, EnvironmentModule.forRoot({}), FileStorageModule.forRootAsync({ useFactory: fileStorageModuleFactory, diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index c3ecf518a0..4e30e84bd8 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -16,20 +16,20 @@ import { } from 'class-validator'; import { EmailDriver } from 'src/engine/core-modules/email/interfaces/email.interface'; +import { AwsRegion } from 'src/engine/core-modules/environment/interfaces/aws-region.interface'; import { NodeEnvironment } from 'src/engine/core-modules/environment/interfaces/node-environment.interface'; +import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface'; import { LLMChatModelDriver } from 'src/engine/core-modules/llm-chat-model/interfaces/llm-chat-model.interface'; import { LLMTracingDriver } from 'src/engine/core-modules/llm-tracing/interfaces/llm-tracing.interface'; -import { AwsRegion } from 'src/engine/core-modules/environment/interfaces/aws-region.interface'; -import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface'; -import { IsDuration } from 'src/engine/core-modules/environment/decorators/is-duration.decorator'; -import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator'; -import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator'; -import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator'; -import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator'; import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum'; import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces'; +import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator'; +import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator'; +import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator'; import { CastToStringArray } from 'src/engine/core-modules/environment/decorators/cast-to-string-array.decorator'; +import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator'; +import { IsDuration } from 'src/engine/core-modules/environment/decorators/is-duration.decorator'; import { IsStrictlyLowerThan } from 'src/engine/core-modules/environment/decorators/is-strictly-lower-than.decorator'; import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces'; import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces'; @@ -88,6 +88,15 @@ export class EnvironmentVariables { @IsBoolean() TELEMETRY_ENABLED = true; + @CastToBoolean() + @IsOptional() + @IsBoolean() + ANALYTICS_ENABLED = false; + + @IsString() + @ValidateIf((env) => env.ANALYTICS_ENABLED) + TINYBIRD_TOKEN: string; + @CastToPositiveNumber() @IsNumber() @IsOptional() diff --git a/packages/twenty-server/src/engine/core-modules/telemetry/telemetry.module.ts b/packages/twenty-server/src/engine/core-modules/telemetry/telemetry.module.ts new file mode 100644 index 0000000000..b9be5ec208 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/telemetry/telemetry.module.ts @@ -0,0 +1,15 @@ +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; + +import { TelemetryService } from './telemetry.service'; + +@Module({ + providers: [TelemetryService], + imports: [ + HttpModule.register({ + baseURL: 'https://t.twenty.com/api/v2', + }), + ], + exports: [TelemetryService], +}) +export class TelemetryModule {} diff --git a/packages/twenty-server/src/engine/core-modules/telemetry/telemetry.service.ts b/packages/twenty-server/src/engine/core-modules/telemetry/telemetry.service.ts new file mode 100644 index 0000000000..6f59f98478 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/telemetry/telemetry.service.ts @@ -0,0 +1,55 @@ +import { HttpService } from '@nestjs/axios'; +import { Injectable, Logger } from '@nestjs/common'; + +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +type CreateEventInput = { + action: string; + payload: object; +}; + +@Injectable() +export class TelemetryService { + private readonly logger = new Logger(TelemetryService.name); + + constructor( + private readonly environmentService: EnvironmentService, + private readonly httpService: HttpService, + ) {} + + async create( + createEventInput: CreateEventInput, + userId: string | null | undefined, + workspaceId: string | null | undefined, + ) { + if (!this.environmentService.get('TELEMETRY_ENABLED')) { + return { success: true }; + } + + const data = { + action: createEventInput.action, + timestamp: new Date().toISOString(), + version: '1', + payload: { + userId: userId, + workspaceId: workspaceId, + ...createEventInput.payload, + }, + }; + + try { + await this.httpService.axiosRef.post(`/selfHostingEvent`, data); + } catch (error) { + this.logger.error('Error occurred:', error); + if (error.response) { + this.logger.error( + `Error response body: ${JSON.stringify(error.response.data)}`, + ); + } + + return { success: false }; + } + + return { success: true }; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 31fdd6379a..4f26a8b0e0 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -5,19 +5,19 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { assert } from 'src/utils/assert'; import { AppToken, AppTokenType, } from 'src/engine/core-modules/app-token/app-token.entity'; +import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { assert } from 'src/utils/assert'; export class UserWorkspaceService extends TypeOrmQueryService { constructor( diff --git a/packages/twenty-server/src/modules/messaging/monitoring/services/messaging-telemetry.service.ts b/packages/twenty-server/src/modules/messaging/monitoring/services/messaging-telemetry.service.ts index efe5ae6b44..d9455ed17b 100644 --- a/packages/twenty-server/src/modules/messaging/monitoring/services/messaging-telemetry.service.ts +++ b/packages/twenty-server/src/modules/messaging/monitoring/services/messaging-telemetry.service.ts @@ -29,8 +29,8 @@ export class MessagingTelemetryService { }: MessagingTelemetryTrackInput): Promise { await this.analyticsService.create( { - type: 'track', - data: { + action: 'monitoring', + payload: { eventName: `messaging.${eventName}`, workspaceId, userId, @@ -41,9 +41,6 @@ export class MessagingTelemetryService { }, userId, workspaceId, - '', // voluntarely not retrieving this - '', // to avoid slowing down - this.environmentService.get('SERVER_URL'), ); } }