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:
committed by
GitHub
parent
f9c076df31
commit
f06cdbdfc6
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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];
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user