Connect EventTracker to TB endpoint (#7240)

#7091 
EventTrackers send information of events to the TinyBird instance:

In order to test:

1. Set ANALYTICS_ENABLED= true and TELEMETRY_ENABLED=true in
evironment-variables.ts
2. Set the TINYBIRD_TOKEN in environment variables (go to TiniyBird
Tokens)
3. Log in to twenty's TinyBird and go to datasources/analytics_events in
twenty_analytics workspace
4. Run twenty and navigate it
5. New events will be logged in the datasources, containing their
timestamp, sessionId and payload.

<img width="1189" alt="Screenshot 2024-09-24 at 17 23 01"
src="https://github.com/user-attachments/assets/85375897-504d-4e75-98e4-98e6a9671f98">
Example of payload when user is not logged in

```
{"hostName":"localhost",
"pathname":"/welcome",
"locale":"en-US",
"userAgent":"Mozilla/5.0",
"href":"http://localhost:3001/welcome",
"referrer":"",
"timeZone":"Europe/Barcelona"}
```
Example of payload when user is logged in
```
{"userId":"2020202",
"workspaceId":"202",
"workspaceDisplayName":"Apple",
"workspaceDomainName":"apple.dev",
"hostName":"localhost",
"pathname":"/objects/companies",
"locale":"en-US",
"userAgent":"Mozilla/5.0Chrome/128.0.0.0Safari/537.36",
"href":"http://localhost:3001/objects/companies",
"referrer":"",
"timeZone":"Europe/Paris"}
```

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Ana Sofia Marin Alexandre 2024-09-26 10:53:10 +02:00 committed by GitHub
parent c9e882f4c0
commit 16bb1f22e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 273 additions and 187 deletions

View File

@ -4,7 +4,10 @@ import { useRecoilValue } from 'recoil';
import { IconCheckbox } from 'twenty-ui'; import { IconCheckbox } from 'twenty-ui';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; 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 { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState'; import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
@ -163,10 +166,14 @@ export const PageChangeEffect = () => {
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setSessionId();
eventTracker('pageview', { 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); }, 500);
}, [eventTracker, location.pathname]); }, [eventTracker, location.pathname]);

View File

@ -165,7 +165,6 @@ export type ClientConfig = {
signInPrefilled: Scalars['Boolean']['output']; signInPrefilled: Scalars['Boolean']['output'];
signUpDisabled: Scalars['Boolean']['output']; signUpDisabled: Scalars['Boolean']['output'];
support: Support; support: Support;
telemetry: Telemetry;
}; };
export type CreateAppTokenInput = { export type CreateAppTokenInput = {
@ -1171,11 +1170,6 @@ export type Support = {
supportFrontChatId?: Maybe<Scalars['String']['output']>; supportFrontChatId?: Maybe<Scalars['String']['output']>;
}; };
export type Telemetry = {
__typename?: 'Telemetry';
enabled: Scalars['Boolean']['output'];
};
export type TimelineCalendarEvent = { export type TimelineCalendarEvent = {
__typename?: 'TimelineCalendarEvent'; __typename?: 'TimelineCalendarEvent';
conferenceLink: LinksMetadata; conferenceLink: LinksMetadata;

View File

@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
export type Maybe<T> = T | null; export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>; export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }; export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -158,7 +158,6 @@ export type ClientConfig = {
signInPrefilled: Scalars['Boolean']; signInPrefilled: Scalars['Boolean'];
signUpDisabled: Scalars['Boolean']; signUpDisabled: Scalars['Boolean'];
support: Support; support: Support;
telemetry: Telemetry;
}; };
export type CreateServerlessFunctionFromFileInput = { export type CreateServerlessFunctionFromFileInput = {
@ -524,6 +523,7 @@ export type MutationSignUpArgs = {
export type MutationTrackArgs = { export type MutationTrackArgs = {
data: Scalars['JSON']; data: Scalars['JSON'];
sessionId: Scalars['String'];
type: Scalars['String']; type: Scalars['String'];
}; };
@ -919,11 +919,6 @@ export type Support = {
supportFrontChatId?: Maybe<Scalars['String']>; supportFrontChatId?: Maybe<Scalars['String']>;
}; };
export type Telemetry = {
__typename?: 'Telemetry';
enabled: Scalars['Boolean'];
};
export type TimelineCalendarEvent = { export type TimelineCalendarEvent = {
__typename?: 'TimelineCalendarEvent'; __typename?: 'TimelineCalendarEvent';
conferenceLink: LinksMetadata; 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 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<{ export type TrackMutationVariables = Exact<{
type: Scalars['String']; action: Scalars['String'];
data: Scalars['JSON']; payload: Scalars['JSON'];
}>; }>;
@ -1511,7 +1506,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; 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; }>; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;
@ -1948,8 +1943,8 @@ export type GetTimelineThreadsFromPersonIdQueryHookResult = ReturnType<typeof us
export type GetTimelineThreadsFromPersonIdLazyQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromPersonIdLazyQuery>; export type GetTimelineThreadsFromPersonIdLazyQueryHookResult = ReturnType<typeof useGetTimelineThreadsFromPersonIdLazyQuery>;
export type GetTimelineThreadsFromPersonIdQueryResult = Apollo.QueryResult<GetTimelineThreadsFromPersonIdQuery, GetTimelineThreadsFromPersonIdQueryVariables>; export type GetTimelineThreadsFromPersonIdQueryResult = Apollo.QueryResult<GetTimelineThreadsFromPersonIdQuery, GetTimelineThreadsFromPersonIdQueryVariables>;
export const TrackDocument = gql` export const TrackDocument = gql`
mutation Track($type: String!, $data: JSON!) { mutation Track($action: String!, $payload: JSON!) {
track(type: $type, data: $data) { track(action: $action, payload: $payload) {
success success
} }
} }
@ -1969,8 +1964,8 @@ export type TrackMutationFn = Apollo.MutationFunction<TrackMutation, TrackMutati
* @example * @example
* const [trackMutation, { data, loading, error }] = useTrackMutation({ * const [trackMutation, { data, loading, error }] = useTrackMutation({
* variables: { * variables: {
* type: // value for 'type' * action: // value for 'type'
* data: // value for 'data' * payload: // value for 'payload'
* }, * },
* }); * });
*/ */
@ -2684,9 +2679,6 @@ export const GetClientConfigDocument = gql`
signInPrefilled signInPrefilled
signUpDisabled signUpDisabled
debugMode debugMode
telemetry {
enabled
}
support { support {
supportDriver supportDriver
supportFrontChatId supportFrontChatId

View File

@ -1,8 +1,8 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const TRACK = gql` export const TRACK = gql`
mutation Track($type: String!, $data: JSON!) { mutation Track($type: String!, $sessionId: String!, $data: JSON!) {
track(type: $type, data: $data) { track(type: $type, sessionId: $sessionId, data: $data) {
success success
} }
} }

View File

@ -1,8 +1,8 @@
import { ReactNode } from 'react';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import { MockedProvider, MockedResponse } from '@apollo/client/testing'; import { MockedProvider, MockedResponse } from '@apollo/client/testing';
import { expect } from '@storybook/test'; import { expect } from '@storybook/test';
import { act, renderHook, waitFor } from '@testing-library/react'; import { act, renderHook, waitFor } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { useEventTracker } from '../useEventTracker'; import { useEventTracker } from '../useEventTracker';
@ -11,15 +11,23 @@ const mocks: MockedResponse[] = [
{ {
request: { request: {
query: gql` query: gql`
mutation Track($type: String!, $data: JSON!) { mutation Track($action: String!, $payload: JSON!) {
track(type: $type, data: $data) { track(action: $action, payload: $payload) {
success success
} }
} }
`, `,
variables: { variables: {
type: 'exampleType', action: 'exampleType',
data: { location: { pathname: '/examplePath' } }, payload: {
sessionId: 'exampleId',
pathname: '',
userAgent: '',
timeZone: '',
locale: '',
href: '',
referrer: '',
},
}, },
}, },
result: jest.fn(() => ({ result: jest.fn(() => ({
@ -43,7 +51,15 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
describe('useEventTracker', () => { describe('useEventTracker', () => {
it('should make the call to track the event', async () => { it('should make the call to track the event', async () => {
const eventType = 'exampleType'; const eventType = 'exampleType';
const eventData = { location: { pathname: '/examplePath' } }; const eventData = {
sessionId: 'exampleId',
pathname: '',
userAgent: '',
timeZone: '',
locale: '',
href: '',
referrer: '',
};
const { result } = renderHook(() => useEventTracker(), { const { result } = renderHook(() => useEventTracker(), {
wrapper: Wrapper, wrapper: Wrapper,
}); });

View File

@ -1,32 +1,46 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { telemetryState } from '@/client-config/states/telemetryState';
import { useTrackMutation } from '~/generated/graphql'; import { useTrackMutation } from '~/generated/graphql';
interface EventLocation {
pathname: string;
}
export interface EventData { 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 = () => { export const useEventTracker = () => {
const telemetry = useRecoilValue(telemetryState);
const [createEventMutation] = useTrackMutation(); const [createEventMutation] = useTrackMutation();
return useCallback( return useCallback(
(eventType: string, eventData: EventData) => { (eventAction: string, eventPayload: EventData) => {
if (telemetry.enabled) { createEventMutation({
createEventMutation({ variables: {
variables: { action: eventAction,
type: eventType, payload: {
data: eventData, sessionId: getSessionId(),
...eventPayload,
}, },
}); },
} });
}, },
[createEventMutation, telemetry], [createEventMutation],
); );
}; };

View File

@ -1,7 +1,7 @@
import { MemoryRouter, useLocation } from 'react-router-dom';
import { ApolloError, gql } from '@apollo/client'; import { ApolloError, gql } from '@apollo/client';
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock'; import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { useApolloFactory } from '../useApolloFactory'; import { useApolloFactory } from '../useApolloFactory';
@ -77,8 +77,8 @@ describe('useApolloFactory', () => {
await act(async () => { await act(async () => {
await result.current.factory.mutate({ await result.current.factory.mutate({
mutation: gql` mutation: gql`
mutation Track($type: String!, $data: JSON!) { mutation Track($type: String!, $sessionId: String!, $data: JSON!) {
track(type: $type, data: $data) { track(type: $type, sessionId: $sessionId, data: $data) {
success success
} }
} }

View File

@ -41,8 +41,8 @@ const makeRequest = async () => {
await client.mutate({ await client.mutate({
mutation: gql` mutation: gql`
mutation Track($type: String!, $data: JSON!) { mutation Track($type: String!, $sessionId: String!, $data: JSON!) {
track(type: $type, data: $data) { track(type: $type, sessionId: $sessionId, data: $data) {
success success
} }
} }

View File

@ -1,8 +1,8 @@
import { ReactNode } from 'react';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { expect } from '@storybook/test'; import { expect } from '@storybook/test';
import { act, renderHook } from '@testing-library/react'; import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil'; import { RecoilRoot, useRecoilValue } from 'recoil';
import { iconsState } from 'twenty-ui'; import { iconsState } from 'twenty-ui';
@ -12,7 +12,6 @@ import { billingState } from '@/client-config/states/billingState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
import { supportChatState } from '@/client-config/states/supportChatState'; import { supportChatState } from '@/client-config/states/supportChatState';
import { telemetryState } from '@/client-config/states/telemetryState';
import { email, mocks, password, results, token } from '../__mocks__/useAuth'; import { email, mocks, password, results, token } from '../__mocks__/useAuth';
@ -81,7 +80,6 @@ describe('useAuth', () => {
const billing = useRecoilValue(billingState); const billing = useRecoilValue(billingState);
const isSignInPrefilled = useRecoilValue(isSignInPrefilledState); const isSignInPrefilled = useRecoilValue(isSignInPrefilledState);
const supportChat = useRecoilValue(supportChatState); const supportChat = useRecoilValue(supportChatState);
const telemetry = useRecoilValue(telemetryState);
const isDebugMode = useRecoilValue(isDebugModeState); const isDebugMode = useRecoilValue(isDebugModeState);
return { return {
...useAuth(), ...useAuth(),
@ -92,7 +90,6 @@ describe('useAuth', () => {
billing, billing,
isSignInPrefilled, isSignInPrefilled,
supportChat, supportChat,
telemetry,
isDebugMode, isDebugMode,
}, },
}; };
@ -126,9 +123,6 @@ describe('useAuth', () => {
supportDriver: 'none', supportDriver: 'none',
supportFrontChatId: null, supportFrontChatId: null,
}); });
expect(state.telemetry).toEqual({
enabled: true,
});
expect(state.isDebugMode).toBe(false); expect(state.isDebugMode).toBe(false);
}); });

View File

@ -21,7 +21,6 @@ import { isClientConfigLoadedState } from '@/client-config/states/isClientConfig
import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
import { supportChatState } from '@/client-config/states/supportChatState'; import { supportChatState } from '@/client-config/states/supportChatState';
import { telemetryState } from '@/client-config/states/telemetryState';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { import {
@ -224,7 +223,6 @@ export const useAuth = () => {
.getLoadable(isSignInPrefilledState) .getLoadable(isSignInPrefilledState)
.getValue(); .getValue();
const supportChat = snapshot.getLoadable(supportChatState).getValue(); const supportChat = snapshot.getLoadable(supportChatState).getValue();
const telemetry = snapshot.getLoadable(telemetryState).getValue();
const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue(); const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue();
const captchaProvider = snapshot const captchaProvider = snapshot
.getLoadable(captchaProviderState) .getLoadable(captchaProviderState)
@ -242,7 +240,6 @@ export const useAuth = () => {
set(billingState, billing); set(billingState, billing);
set(isSignInPrefilledState, isSignInPrefilled); set(isSignInPrefilledState, isSignInPrefilled);
set(supportChatState, supportChat); set(supportChatState, supportChat);
set(telemetryState, telemetry);
set(isDebugModeState, isDebugMode); set(isDebugModeState, isDebugMode);
set(captchaProviderState, captchaProvider); set(captchaProviderState, captchaProvider);
set(isClientConfigLoadedState, isClientConfigLoaded); set(isClientConfigLoadedState, isClientConfigLoaded);

View File

@ -12,7 +12,6 @@ import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilled
import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState'; import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState'; import { supportChatState } from '@/client-config/states/supportChatState';
import { telemetryState } from '@/client-config/states/telemetryState';
import { useGetClientConfigQuery } from '~/generated/graphql'; import { useGetClientConfigQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -24,7 +23,6 @@ export const ClientConfigProviderEffect = () => {
const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState); const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState);
const setBilling = useSetRecoilState(billingState); const setBilling = useSetRecoilState(billingState);
const setTelemetry = useSetRecoilState(telemetryState);
const setSupportChat = useSetRecoilState(supportChatState); const setSupportChat = useSetRecoilState(supportChatState);
const setSentryConfig = useSetRecoilState(sentryConfigState); const setSentryConfig = useSetRecoilState(sentryConfigState);
@ -56,7 +54,6 @@ export const ClientConfigProviderEffect = () => {
setIsSignUpDisabled(data?.clientConfig.signUpDisabled); setIsSignUpDisabled(data?.clientConfig.signUpDisabled);
setBilling(data?.clientConfig.billing); setBilling(data?.clientConfig.billing);
setTelemetry(data?.clientConfig.telemetry);
setSupportChat(data?.clientConfig.support); setSupportChat(data?.clientConfig.support);
setSentryConfig({ setSentryConfig({
@ -79,7 +76,6 @@ export const ClientConfigProviderEffect = () => {
setIsDebugMode, setIsDebugMode,
setIsSignInPrefilled, setIsSignInPrefilled,
setIsSignUpDisabled, setIsSignUpDisabled,
setTelemetry,
setSupportChat, setSupportChat,
setBilling, setBilling,
setSentryConfig, setSentryConfig,

View File

@ -16,9 +16,6 @@ export const GET_CLIENT_CONFIG = gql`
signInPrefilled signInPrefilled
signUpDisabled signUpDisabled
debugMode debugMode
telemetry {
enabled
}
support { support {
supportDriver supportDriver
supportFrontChatId supportFrontChatId

View File

@ -1,8 +0,0 @@
import { createState } from 'twenty-ui';
import { Telemetry } from '~/generated/graphql';
export const telemetryState = createState<Telemetry>({
key: 'telemetryState',
defaultValue: { enabled: true },
});

View File

@ -13,10 +13,6 @@ export const mockedClientConfig: ClientConfig = {
microsoft: false, microsoft: false,
__typename: 'AuthProviders', __typename: 'AuthProviders',
}, },
telemetry: {
enabled: false,
__typename: 'Telemetry',
},
support: { support: {
supportDriver: 'front', supportDriver: 'front',
supportFrontChatId: null, supportFrontChatId: null,

View File

@ -2,15 +2,15 @@ import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service'; 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 { 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'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type';
@Injectable() @Injectable()
export class TelemetryListener { export class TelemetryListener {
constructor( constructor(
private readonly analyticsService: AnalyticsService, private readonly analyticsService: AnalyticsService,
private readonly environmentService: EnvironmentService, private readonly telemetryService: TelemetryService,
) {} ) {}
@OnEvent('*.created') @OnEvent('*.created')
@ -21,16 +21,11 @@ export class TelemetryListener {
payload.events.map((eventPayload) => payload.events.map((eventPayload) =>
this.analyticsService.create( this.analyticsService.create(
{ {
type: 'track', action: payload.name,
data: { payload: {},
eventName: payload.name,
},
}, },
eventPayload.userId, eventPayload.userId,
payload.workspaceId, payload.workspaceId,
'', // voluntarily not retrieving this
'', // to avoid slowing down
this.environmentService.get('SERVER_URL'),
), ),
), ),
); );
@ -41,21 +36,29 @@ export class TelemetryListener {
payload: WorkspaceEventBatch<ObjectRecordCreateEvent<any>>, payload: WorkspaceEventBatch<ObjectRecordCreateEvent<any>>,
) { ) {
await Promise.all( await Promise.all(
payload.events.map((eventPayload) => payload.events.map(async (eventPayload) => {
this.analyticsService.create( this.analyticsService.create(
{ {
type: 'track', action: 'user.signup',
data: { payload: {},
eventName: 'user.signup', },
eventPayload.userId,
payload.workspaceId,
);
this.telemetryService.create(
{
action: 'user.signup',
payload: {
payload,
userId: undefined,
workspaceId: undefined,
}, },
}, },
eventPayload.userId, eventPayload.userId,
payload.workspaceId, payload.workspaceId,
'', );
'', }),
this.environmentService.get('SERVER_URL'),
),
),
); );
} }
} }

View File

@ -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 { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileModule } from 'src/engine/core-modules/file/file.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 { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; 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]), ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
AnalyticsModule, AnalyticsModule,
TelemetryModule,
DuplicateModule, DuplicateModule,
FileModule, FileModule,
FeatureFlagModule, FeatureFlagModule,

View File

@ -1,14 +1,16 @@
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { AnalyticsService } from './analytics.service';
import { AnalyticsResolver } from './analytics.resolver'; import { AnalyticsResolver } from './analytics.resolver';
import { AnalyticsService } from './analytics.service';
const TINYBIRD_BASE_URL = 'https://api.eu-central-1.aws.tinybird.co/v0';
@Module({ @Module({
providers: [AnalyticsResolver, AnalyticsService], providers: [AnalyticsResolver, AnalyticsService],
imports: [ imports: [
HttpModule.register({ HttpModule.register({
baseURL: 'https://t.twenty.com/api/v1/s2s', baseURL: TINYBIRD_BASE_URL,
}), }),
], ],
exports: [AnalyticsService], exports: [AnalyticsService],

View File

@ -31,9 +31,6 @@ export class AnalyticsResolver {
createAnalyticsInput, createAnalyticsInput,
user?.id, user?.id,
workspace?.id, workspace?.id,
workspace?.displayName,
workspace?.domainName,
this.environmentService.get('SERVER_URL') ?? request.hostname,
); );
} }
} }

View File

@ -1,16 +1,19 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios'; 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'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
type CreateEventInput = { type CreateEventInput = {
type: string; action: string;
data: object; payload: object;
}; };
@Injectable() @Injectable()
export class AnalyticsService { export class AnalyticsService {
private readonly logger = new Logger(AnalyticsService.name); private readonly logger = new Logger(AnalyticsService.name);
private readonly datasource = 'event';
constructor( constructor(
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
@ -21,30 +24,42 @@ export class AnalyticsService {
createEventInput: CreateEventInput, createEventInput: CreateEventInput,
userId: string | null | undefined, userId: string | null | undefined,
workspaceId: 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 }; return { success: true };
} }
const data = { const data = {
type: createEventInput.type, action: createEventInput.action,
data: { timestamp: new Date().toISOString(),
hostname: hostName, version: '1',
userUUID: userId, payload: {
workspaceUUID: workspaceId, userId: userId,
workspaceDisplayName: workspaceDisplayName, workspaceId: workspaceId,
workspaceDomainName: workspaceDomainName, ...createEventInput.payload,
...createEventInput.data, },
};
const config: AxiosRequestConfig = {
headers: {
Authorization:
'Bearer ' + this.environmentService.get('TINYBIRD_TOKEN'),
}, },
}; };
try { try {
await this.httpService.axiosRef.post('/v1', data); await this.httpService.axiosRef.post(
} catch { `/events?name=${this.datasource}`,
this.logger.error('Failed to send analytics event'); 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 }; return { success: false };
} }

View File

@ -1,16 +1,16 @@
import { ArgsType, Field } from '@nestjs/graphql'; import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsString } from 'class-validator';
import graphqlTypeJson from 'graphql-type-json'; import graphqlTypeJson from 'graphql-type-json';
import { IsNotEmpty, IsString, IsObject } from 'class-validator';
@ArgsType() @ArgsType()
export class CreateAnalyticsInput { export class CreateAnalyticsInput {
@Field({ description: 'Type of the event' }) @Field({ description: 'Type of the event' })
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
type: string; action: string;
@Field(() => graphqlTypeJson, { description: 'Event data in JSON format' }) @Field(() => graphqlTypeJson, { description: 'Event payload in JSON format' })
@IsObject() @IsObject()
data: JSON; payload: JSON;
} }

View File

@ -76,9 +76,6 @@ export class ClientConfig {
@Field(() => AuthProviders, { nullable: false }) @Field(() => AuthProviders, { nullable: false })
authProviders: AuthProviders; authProviders: AuthProviders;
@Field(() => Telemetry, { nullable: false })
telemetry: Telemetry;
@Field(() => Billing, { nullable: false }) @Field(() => Billing, { nullable: false })
billing: Billing; billing: Billing;

View File

@ -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'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@ -17,9 +17,6 @@ export class ClientConfigResolver {
password: this.environmentService.get('AUTH_PASSWORD_ENABLED'), password: this.environmentService.get('AUTH_PASSWORD_ENABLED'),
microsoft: this.environmentService.get('AUTH_MICROSOFT_ENABLED'), microsoft: this.environmentService.get('AUTH_MICROSOFT_ENABLED'),
}, },
telemetry: {
enabled: this.environmentService.get('TELEMETRY_ENABLED'),
},
billing: { billing: {
isBillingEnabled: this.environmentService.get('IS_BILLING_ENABLED'), isBillingEnabled: this.environmentService.get('IS_BILLING_ENABLED'),
billingUrl: this.environmentService.get('BILLING_PLAN_REQUIRED_LINK'), billingUrl: this.environmentService.get('BILLING_PLAN_REQUIRED_LINK'),

View File

@ -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 { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.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 { 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 { CaptchaModule } from 'src/engine/core-modules/captcha/captcha.module';
import { captchaModuleFactory } from 'src/engine/core-modules/captcha/captcha.module-factory'; 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 { 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 { 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 { 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 { 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 { 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 { 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 { AnalyticsModule } from './analytics/analytics.module';
import { ClientConfigModule } from './client-config/client-config.module';
import { FileModule } from './file/file.module';
@Module({ @Module({
imports: [ imports: [
@ -66,6 +67,7 @@ import { AnalyticsModule } from './analytics/analytics.module';
WorkflowTriggerApiModule, WorkflowTriggerApiModule,
WorkspaceEventEmitterModule, WorkspaceEventEmitterModule,
ActorModule, ActorModule,
TelemetryModule,
EnvironmentModule.forRoot({}), EnvironmentModule.forRoot({}),
FileStorageModule.forRootAsync({ FileStorageModule.forRootAsync({
useFactory: fileStorageModuleFactory, useFactory: fileStorageModuleFactory,

View File

@ -16,20 +16,20 @@ import {
} from 'class-validator'; } from 'class-validator';
import { EmailDriver } from 'src/engine/core-modules/email/interfaces/email.interface'; 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 { 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 { 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 { 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 { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces'; 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 { 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 { IsStrictlyLowerThan } from 'src/engine/core-modules/environment/decorators/is-strictly-lower-than.decorator';
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces'; import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces'; import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces';
@ -88,6 +88,15 @@ export class EnvironmentVariables {
@IsBoolean() @IsBoolean()
TELEMETRY_ENABLED = true; TELEMETRY_ENABLED = true;
@CastToBoolean()
@IsOptional()
@IsBoolean()
ANALYTICS_ENABLED = false;
@IsString()
@ValidateIf((env) => env.ANALYTICS_ENABLED)
TINYBIRD_TOKEN: string;
@CastToPositiveNumber() @CastToPositiveNumber()
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()

View File

@ -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 {}

View File

@ -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 };
}
}

View File

@ -5,19 +5,19 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { TypeORMService } from 'src/database/typeorm/typeorm.service'; 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 { import {
AppToken, AppToken,
AppTokenType, AppTokenType,
} from 'src/engine/core-modules/app-token/app-token.entity'; } 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 { 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<UserWorkspace> { export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
constructor( constructor(

View File

@ -29,8 +29,8 @@ export class MessagingTelemetryService {
}: MessagingTelemetryTrackInput): Promise<void> { }: MessagingTelemetryTrackInput): Promise<void> {
await this.analyticsService.create( await this.analyticsService.create(
{ {
type: 'track', action: 'monitoring',
data: { payload: {
eventName: `messaging.${eventName}`, eventName: `messaging.${eventName}`,
workspaceId, workspaceId,
userId, userId,
@ -41,9 +41,6 @@ export class MessagingTelemetryService {
}, },
userId, userId,
workspaceId, workspaceId,
'', // voluntarely not retrieving this
'', // to avoid slowing down
this.environmentService.get('SERVER_URL'),
); );
} }
} }