mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-23 14:03:35 +03:00
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:
parent
c9e882f4c0
commit
16bb1f22e4
@ -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]);
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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],
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -16,9 +16,6 @@ export const GET_CLIENT_CONFIG = gql`
|
|||||||
signInPrefilled
|
signInPrefilled
|
||||||
signUpDisabled
|
signUpDisabled
|
||||||
debugMode
|
debugMode
|
||||||
telemetry {
|
|
||||||
enabled
|
|
||||||
}
|
|
||||||
support {
|
support {
|
||||||
supportDriver
|
supportDriver
|
||||||
supportFrontChatId
|
supportFrontChatId
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { Telemetry } from '~/generated/graphql';
|
|
||||||
|
|
||||||
export const telemetryState = createState<Telemetry>({
|
|
||||||
key: 'telemetryState',
|
|
||||||
defaultValue: { enabled: true },
|
|
||||||
});
|
|
@ -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,
|
||||||
|
@ -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'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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],
|
||||||
|
@ -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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
@ -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 {}
|
@ -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 };
|
||||||
|
}
|
||||||
|
}
|
@ -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(
|
||||||
|
@ -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'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user