refactor webhookAnalytics call and enrich analytics module (#8253)

**TLDR**

Refactor WebhoonAnalytics Graph to a more abstract version
AnalyticsGraph (in analytics module). Thus enabling the components to be
used on different instances (ex: new endpoint, new kind of graph).

**In order to test:**

1. Set ANALYTICS_ENABLED to true
2. Set TINYBIRD_JWT_TOKEN to the ADMIN token from the workspace
twenty_analytics_playground
3. Set TINYBIRD_JWT_TOKEN to the datasource or your admin token from the
workspace twenty_analytics_playground
4. Create a Webhook in twenty and set wich events it needs to track
5. Run twenty-worker in order to make the webhooks work.
6. Do your tasks in order to populate the data
7. Enter to settings> webhook>your webhook and the statistics section
should be displayed.

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Ana Sofia Marin Alexandre
2024-11-08 06:00:51 -03:00
committed by GitHub
parent f9c076df31
commit f06cdbdfc6
62 changed files with 1429 additions and 539 deletions

View File

@ -0,0 +1,87 @@
import { useAnalyticsTinybirdJwts } from '@/analytics/hooks/useAnalyticsTinybirdJwts';
import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
import { act, renderHook } from '@testing-library/react';
import { useSetRecoilState } from 'recoil';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
describe('useAnalyticsTinybirdJwts', () => {
const JWT_NAME = 'getWebhookAnalytics';
const TEST_JWT_TOKEN = 'test-jwt-token';
it('should return undefined when no user is logged in', () => {
const { result } = renderHook(
() => {
const setCurrentUserState = useSetRecoilState(currentUserState);
return {
hook: useAnalyticsTinybirdJwts(JWT_NAME),
setCurrentUserState,
};
},
{ wrapper: Wrapper },
);
act(() => {
result.current.setCurrentUserState(null);
});
expect(result.current.hook).toBeUndefined();
});
it('should return the correct JWT token when available', () => {
const { result } = renderHook(
() => {
const setCurrentUserState = useSetRecoilState(currentUserState);
return {
hook: useAnalyticsTinybirdJwts(JWT_NAME),
setCurrentUserState,
};
},
{ wrapper: Wrapper },
);
act(() => {
result.current.setCurrentUserState({
id: '1',
email: 'test@test.com',
canImpersonate: false,
userVars: {},
analyticsTinybirdJwts: {
[JWT_NAME]: TEST_JWT_TOKEN,
},
} as CurrentUser);
});
expect(result.current.hook).toBe(TEST_JWT_TOKEN);
});
it('should return undefined when JWT token is not available', () => {
const { result } = renderHook(
() => {
const setCurrentUserState = useSetRecoilState(currentUserState);
return {
hook: useAnalyticsTinybirdJwts(JWT_NAME),
setCurrentUserState,
};
},
{ wrapper: Wrapper },
);
act(() => {
result.current.setCurrentUserState({
id: '1',
email: 'test@test.com',
canImpersonate: false,
userVars: {},
analyticsTinybirdJwts: {
getPageviewsAnalytics: TEST_JWT_TOKEN,
},
} as CurrentUser);
});
expect(result.current.hook).toBeUndefined();
});
});

View File

@ -0,0 +1,87 @@
import { useAnalyticsTinybirdJwts } from '@/analytics/hooks/useAnalyticsTinybirdJwts';
import { useGraphData } from '@/analytics/hooks/useGraphData';
import { fetchGraphDataOrThrow } from '@/analytics/utils/fetchGraphDataOrThrow';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { renderHook } from '@testing-library/react';
jest.mock('@/analytics/hooks/useAnalyticsTinybirdJwts');
jest.mock('@/analytics/utils/fetchGraphDataOrThrow');
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
describe('useGraphData', () => {
const mockEnqueueSnackBar = jest.fn();
const mockUseSnackBar = jest.fn().mockReturnValue({
enqueueSnackBar: mockEnqueueSnackBar,
});
const mockUseAnalyticsTinybirdJwts = jest.fn();
const mockFetchGraphDataOrThrow = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useSnackBar as jest.MockedFunction<typeof useSnackBar>).mockImplementation(
mockUseSnackBar,
);
(
useAnalyticsTinybirdJwts as jest.MockedFunction<
typeof useAnalyticsTinybirdJwts
>
).mockImplementation(mockUseAnalyticsTinybirdJwts);
(
fetchGraphDataOrThrow as jest.MockedFunction<typeof fetchGraphDataOrThrow>
).mockImplementation(mockFetchGraphDataOrThrow);
});
it('should fetch graph data successfully', async () => {
const mockJwt = 'mock-jwt';
const mockRecordId = 'mock-record-id';
const mockEndpointName = 'getWebhookAnalytics';
const mockGraphData = [{ x: '2023-01-01', y: 100 }];
mockUseAnalyticsTinybirdJwts.mockReturnValue(mockJwt);
mockFetchGraphDataOrThrow.mockResolvedValue(mockGraphData);
const { result } = renderHook(() =>
useGraphData({ recordId: mockRecordId, endpointName: mockEndpointName }),
);
const { fetchGraphData } = result.current;
const data = await fetchGraphData('7D');
expect(data).toEqual(mockGraphData);
expect(mockFetchGraphDataOrThrow).toHaveBeenCalledWith({
recordId: mockRecordId,
windowLength: '7D',
tinybirdJwt: mockJwt,
endpointName: mockEndpointName,
});
expect(mockEnqueueSnackBar).not.toHaveBeenCalled();
});
it('should handle errors when fetching graph data', async () => {
const mockRecordId = 'mock-record-id';
const mockEndpointName = 'getWebhookAnalytics';
const mockError = new Error('Something went wrong');
mockUseAnalyticsTinybirdJwts.mockReturnValue('');
mockFetchGraphDataOrThrow.mockRejectedValue(mockError);
const { result } = renderHook(() =>
useGraphData({ recordId: mockRecordId, endpointName: mockEndpointName }),
);
const { fetchGraphData } = result.current;
const data = await fetchGraphData('7D');
expect(data).toEqual([]);
expect(mockFetchGraphDataOrThrow).toHaveBeenCalledWith({
recordId: mockRecordId,
windowLength: '7D',
tinybirdJwt: '',
endpointName: mockEndpointName,
});
expect(mockEnqueueSnackBar).toHaveBeenCalledWith(
'Something went wrong while fetching webhook usage: Something went wrong',
{
variant: SnackBarVariant.Error,
},
);
});
});

View File

@ -0,0 +1,16 @@
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';
export const useAnalyticsTinybirdJwts = (
jwtName: keyof AnalyticsTinybirdJwtMap,
): string | undefined => {
const currentUser = useRecoilValue(currentUserState);
if (!currentUser) {
return undefined;
}
return currentUser.analyticsTinybirdJwts?.[jwtName];
};

View File

@ -0,0 +1,42 @@
import { useAnalyticsTinybirdJwts } from '@/analytics/hooks/useAnalyticsTinybirdJwts';
import { AnalyticsComponentProps as useGraphDataProps } from '@/analytics/types/AnalyticsComponentProps';
import { fetchGraphDataOrThrow } from '@/analytics/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';
import { useCallback } from 'react';
export const useGraphData = ({ recordId, endpointName }: useGraphDataProps) => {
const { enqueueSnackBar } = useSnackBar();
const tinybirdJwt = useAnalyticsTinybirdJwts(endpointName);
const fetchGraphData = useCallback(
async (windowLengthGraphOption: '7D' | '1D' | '12H' | '4H') => {
try {
if (isUndefined(tinybirdJwt)) {
throw new Error('No jwt associated with this endpoint found');
}
return await fetchGraphDataOrThrow({
recordId,
windowLength: windowLengthGraphOption,
tinybirdJwt,
endpointName,
});
} catch (error) {
if (error instanceof Error) {
enqueueSnackBar(
`Something went wrong while fetching webhook usage: ${error.message}`,
{
variant: SnackBarVariant.Error,
},
);
}
return [];
}
},
[tinybirdJwt, recordId, endpointName, enqueueSnackBar],
);
return { fetchGraphData };
};