diff --git a/.github/workflows/ci-chrome-extension.yaml b/.github/workflows/ci-chrome-extension.yaml index 15d99938b..6f99f922a 100644 --- a/.github/workflows/ci-chrome-extension.yaml +++ b/.github/workflows/ci-chrome-extension.yaml @@ -34,10 +34,10 @@ jobs: packages/twenty-chrome-extension/** - name: Install dependencies - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Chrome Extension / Run build - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx build twenty-chrome-extension - name: Mark as Valid if No Changes diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index 101e1df3b..9b24cacc4 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -37,30 +37,33 @@ jobs: id: changed-files uses: tj-actions/changed-files@v11 with: - files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**' + files: | + package.json + packages/twenty-server/** + packages/twenty-emails/** - name: Install dependencies - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run lint & typecheck - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend tasks: lint,typecheck - name: Server / Build - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx build twenty-server - name: Server / Write .env - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx reset:env twenty-server - name: Worker / Run - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx run twenty-server:worker:ci server-test: @@ -78,18 +81,21 @@ jobs: id: changed-files uses: tj-actions/changed-files@v11 with: - files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**' + files: | + package.json + packages/twenty-server/** + packages/twenty-emails/** - name: Install dependencies - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run Tests - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend @@ -122,18 +128,21 @@ jobs: id: changed-files uses: tj-actions/changed-files@v11 with: - files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**' + files: | + package.json + packages/twenty-server/** + packages/twenty-emails/** - name: Install dependencies - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run Integration Tests - if: steps.changed-files.outputs.changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend diff --git a/.vscode/settings.json b/.vscode/settings.json index d63c92973..fde6cdfb1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,5 +45,5 @@ "search.exclude": { "**/.yarn": true, }, - "eslint.debug": true + "eslint.debug": true, } diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 7f053fc6b..aab2dbddd 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1058,6 +1058,7 @@ export type UpdateWorkspaceInput = { export type User = { __typename?: 'User'; + analyticsTinybirdJwt?: Maybe; canImpersonate: Scalars['Boolean']; createdAt: Scalars['DateTime']; defaultAvatarUrl?: Maybe; @@ -1520,7 +1521,7 @@ export type ImpersonateMutationVariables = Exact<{ }>; -export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type RenewTokenMutationVariables = Exact<{ appToken: Scalars['String']; @@ -1553,7 +1554,7 @@ export type VerifyMutationVariables = Exact<{ }>; -export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type CheckUserExistsQueryVariables = Exact<{ email: Scalars['String']; @@ -1607,7 +1608,7 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string] export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -1624,7 +1625,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -1825,6 +1826,7 @@ export const UserQueryFragmentFragmentDoc = gql` email canImpersonate supportUserHash + analyticsTinybirdJwt onboardingStatus workspaceMember { ...WorkspaceMemberQueryFragment diff --git a/packages/twenty-front/src/modules/auth/states/currentUserState.ts b/packages/twenty-front/src/modules/auth/states/currentUserState.ts index 2feedc94f..352013254 100644 --- a/packages/twenty-front/src/modules/auth/states/currentUserState.ts +++ b/packages/twenty-front/src/modules/auth/states/currentUserState.ts @@ -7,6 +7,7 @@ export type CurrentUser = Pick< | 'id' | 'email' | 'supportUserHash' + | 'analyticsTinybirdJwt' | 'canImpersonate' | 'onboardingStatus' | 'userVars' diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx index 6d4fd06dc..322ec64cf 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx @@ -1,6 +1,6 @@ import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData'; import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useSetRecoilState } from 'recoil'; type SettingsDevelopersWebhookUsageGraphEffectProps = { @@ -11,14 +11,18 @@ export const SettingsDevelopersWebhookUsageGraphEffect = ({ webhookId, }: SettingsDevelopersWebhookUsageGraphEffectProps) => { const setWebhookGraphData = useSetRecoilState(webhookGraphDataState); + const [isLoaded, setIsLoaded] = useState(false); const { fetchGraphData } = useGraphData(webhookId); useEffect(() => { - fetchGraphData('7D').then((graphInput) => { - setWebhookGraphData(graphInput); - }); - }, [fetchGraphData, setWebhookGraphData, webhookId]); + if (!isLoaded) { + fetchGraphData('7D').then((graphInput) => { + setWebhookGraphData(graphInput); + }); + setIsLoaded(true); + } + }, [fetchGraphData, isLoaded, setWebhookGraphData, webhookId]); return <>; }; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx new file mode 100644 index 000000000..70eb919a1 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/__tests__/useAnalyticsTinybirdJwt.test.tsx @@ -0,0 +1,47 @@ +import { renderHook } from '@testing-library/react'; + +import { CurrentUser, currentUserState } from '@/auth/states/currentUserState'; +import { useAnalyticsTinybirdJwt } from '@/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt'; +import { act } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; + +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + +describe('useAnalyticsTinybirdJwt', () => { + it('should return the analytics jwt token', async () => { + const { result } = renderHook( + () => { + const setCurrentUserState = useSetRecoilState(currentUserState); + + return { + useAnalyticsTinybirdJwt: useAnalyticsTinybirdJwt(), + setCurrentUserState, + }; + }, + { wrapper: Wrapper }, + ); + + act(() => { + result.current.setCurrentUserState({ + analyticsTinybirdJwt: 'jwt', + } as CurrentUser); + }); + + expect(result.current.useAnalyticsTinybirdJwt).toBe('jwt'); + + act(() => { + result.current.setCurrentUserState(null); + }); + + expect(result.current.useAnalyticsTinybirdJwt).toBeUndefined(); + + act(() => { + result.current.setCurrentUserState({} as CurrentUser); + }); + + expect(result.current.useAnalyticsTinybirdJwt).toBeUndefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts new file mode 100644 index 000000000..67fbf1760 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt.ts @@ -0,0 +1,18 @@ +import { useRecoilValue } from 'recoil'; + +import { currentUserState } from '@/auth/states/currentUserState'; +import { isNull } from '@sniptt/guards'; + +export const useAnalyticsTinybirdJwt = (): string | undefined => { + const currentUser = useRecoilValue(currentUserState); + + if (!currentUser) { + return undefined; + } + + if (isNull(currentUser.analyticsTinybirdJwt)) { + return undefined; + } + + return currentUser.analyticsTinybirdJwt; +}; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx index 62fc6d4ad..d141d8273 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx +++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx @@ -1,16 +1,24 @@ +import { useAnalyticsTinybirdJwt } from '@/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt'; import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { isUndefined } from '@sniptt/guards'; export const useGraphData = (webhookId: string) => { const { enqueueSnackBar } = useSnackBar(); + const analyticsTinybirdJwt = useAnalyticsTinybirdJwt(); const fetchGraphData = async ( windowLengthGraphOption: '7D' | '1D' | '12H' | '4H', ) => { try { + if (isUndefined(analyticsTinybirdJwt)) { + throw new Error('No analyticsTinybirdJwt found'); + } + return await fetchGraphDataOrThrow({ webhookId, windowLength: windowLengthGraphOption, + tinybirdJwt: analyticsTinybirdJwt, }); } catch (error) { enqueueSnackBar('Something went wrong while fetching webhook usage', { diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts index b7123f712..4b66c4b9b 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts +++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts @@ -4,22 +4,24 @@ import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/con type fetchGraphDataOrThrowProps = { webhookId: string; windowLength: '7D' | '1D' | '12H' | '4H'; + tinybirdJwt: string; }; export const fetchGraphDataOrThrow = async ({ webhookId, windowLength, + tinybirdJwt, }: fetchGraphDataOrThrowProps) => { const queryString = new URLSearchParams({ ...WEBHOOK_GRAPH_API_OPTIONS_MAP[windowLength], webhookIdRequest: webhookId, }).toString(); - const token = 'REPLACE_ME'; + const response = await fetch( `https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?${queryString}`, { headers: { - Authorization: 'Bearer ' + token, + Authorization: 'Bearer ' + tinybirdJwt, }, }, ); diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index 76e5400c3..8cdb26be8 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -8,6 +8,7 @@ export const USER_QUERY_FRAGMENT = gql` email canImpersonate supportUserHash + analyticsTinybirdJwt onboardingStatus workspaceMember { ...WorkspaceMemberQueryFragment diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts index 2b4c8705d..3897a136e 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.module.ts @@ -1,6 +1,8 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; +import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; + import { AnalyticsResolver } from './analytics.resolver'; import { AnalyticsService } from './analytics.service'; @@ -9,6 +11,7 @@ const TINYBIRD_BASE_URL = 'https://api.eu-central-1.aws.tinybird.co/v0'; @Module({ providers: [AnalyticsResolver, AnalyticsService], imports: [ + JwtModule, HttpModule.register({ baseURL: TINYBIRD_BASE_URL, }), diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.spec.ts index ba23dc9a2..3d700198e 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.spec.ts @@ -1,7 +1,4 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { HttpService } from '@nestjs/axios'; - -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { AnalyticsResolver } from './analytics.resolver'; import { AnalyticsService } from './analytics.service'; @@ -13,13 +10,8 @@ describe('AnalyticsResolver', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AnalyticsResolver, - AnalyticsService, { - provide: EnvironmentService, - useValue: {}, - }, - { - provide: HttpService, + provide: AnalyticsService, useValue: {}, }, ], diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts index 71e76f8bd..9c6cd3090 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.resolver.ts @@ -1,6 +1,5 @@ import { Args, Mutation, Resolver } from '@nestjs/graphql'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; @@ -13,10 +12,7 @@ import { CreateAnalyticsInput } from './dtos/create-analytics.input'; @Resolver(() => Analytics) export class AnalyticsResolver { - constructor( - private readonly analyticsService: AnalyticsService, - private readonly environmentService: EnvironmentService, - ) {} + constructor(private readonly analyticsService: AnalyticsService) {} @Mutation(() => Analytics) track( diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.spec.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.spec.ts index a22ede293..36feaed4d 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.spec.ts @@ -1,7 +1,8 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { HttpService } from '@nestjs/axios'; +import { Test, TestingModule } from '@nestjs/testing'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; import { AnalyticsService } from './analytics.service'; @@ -16,6 +17,10 @@ describe('AnalyticsService', () => { provide: EnvironmentService, useValue: {}, }, + { + provide: JwtWrapperService, + useValue: {}, + }, { provide: HttpService, useValue: {}, diff --git a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts index 2b2eeb3d6..49980b549 100644 --- a/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts +++ b/packages/twenty-server/src/engine/core-modules/analytics/analytics.service.ts @@ -4,6 +4,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { AxiosRequestConfig } from 'axios'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service'; type CreateEventInput = { action: string; @@ -16,6 +17,7 @@ export class AnalyticsService { private readonly defaultDatasource = 'event'; constructor( + private readonly jwtWrapperService: JwtWrapperService, private readonly environmentService: EnvironmentService, private readonly httpService: HttpService, ) {} @@ -58,7 +60,7 @@ export class AnalyticsService { const config: AxiosRequestConfig = { headers: { Authorization: - 'Bearer ' + this.environmentService.get('TINYBIRD_TOKEN'), + 'Bearer ' + this.environmentService.get('TINYBIRD_INGEST_TOKEN'), }, }; @@ -86,4 +88,25 @@ export class AnalyticsService { return { success: true }; } + + async generateWorkspaceJwt(workspaceId: string | undefined) { + const pipeId = 't_b49e0fe60f9e438eae81cb31c5260df2'; // refactor this pass as params + //perhaps a constant of name:pipeId??? better typing in this func^ + const payload = { + name: 'my_demo_jwt', + workspace_id: this.environmentService.get('TINYBIRD_WORKSPACE_UUID'), + scopes: [ + { + type: 'PIPES:READ', + resource: pipeId, + fixed_params: { workspaceId: workspaceId }, + }, + ], + }; + + return this.jwtWrapperService.sign(payload, { + secret: this.environmentService.get('TINYBIRD_GENERATE_JWT_TOKEN'), + expiresIn: '7d', + }); + } } diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index faab8c9d6..03b0d234e 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -95,7 +95,15 @@ export class EnvironmentVariables { @IsString() @ValidateIf((env) => env.ANALYTICS_ENABLED) - TINYBIRD_TOKEN: string; + TINYBIRD_INGEST_TOKEN: string; + + @IsString() + @ValidateIf((env) => env.ANALYTICS_ENABLED) + TINYBIRD_WORKSPACE_UUID: string; + + @IsString() + @ValidateIf((env) => env.ANALYTICS_ENABLED) + TINYBIRD_GENERATE_JWT_TOKEN: string; @CastToPositiveNumber() @IsNumber() diff --git a/packages/twenty-server/src/engine/core-modules/user/user.module.ts b/packages/twenty-server/src/engine/core-modules/user/user.module.ts index 7a776cca2..57ea2140c 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.module.ts @@ -7,6 +7,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; @@ -37,6 +38,7 @@ import { UserService } from './services/user.service'; OnboardingModule, TypeOrmModule.forFeature([KeyValuePair], 'core'), UserVarsModule, + AnalyticsModule, ], exports: [UserService], providers: [UserService, UserResolver, TypeORMService], diff --git a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts index d4304622a..86f27d821 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.resolver.ts @@ -19,6 +19,7 @@ import { Repository } from 'typeorm'; import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; +import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; @@ -55,6 +56,7 @@ export class UserResolver { private readonly onboardingService: OnboardingService, private readonly userVarService: UserVarsService, private readonly fileService: FileService, + private readonly analyticsService: AnalyticsService, ) {} @Query(() => User) @@ -154,6 +156,15 @@ export class UserResolver { return getHMACKey(parent.email, key); } + @ResolveField(() => String, { + nullable: true, + }) + async analyticsTinybirdJwt( + @AuthWorkspace() workspace: Workspace | undefined, + ): Promise { + return await this.analyticsService.generateWorkspaceJwt(workspace?.id); + } + @Mutation(() => String) async uploadProfilePicture( @AuthUser() { id }: User,