Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor webhookAnalytics call and enrich analytics module #8253

Merged
merged 13 commits into from
Nov 8, 2024
10 changes: 9 additions & 1 deletion packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ export type Analytics = {
success: Scalars['Boolean']['output'];
};

export type AnalyticsTinybirdJwtMap = {
__typename?: 'AnalyticsTinybirdJwtMap';
getPageviewsAnalytics: Scalars['String']['output'];
getUsersAnalytics: Scalars['String']['output'];
getWebhookAnalytics: Scalars['String']['output'];
};

export type ApiConfig = {
__typename?: 'ApiConfig';
mutationMaximumAffectedRecords: Scalars['Float']['output'];
Expand Down Expand Up @@ -1490,7 +1497,7 @@ export type UpdateWorkspaceInput = {

export type User = {
__typename?: 'User';
analyticsTinybirdJwt?: Maybe<Scalars['String']['output']>;
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
canImpersonate: Scalars['Boolean']['output'];
createdAt: Scalars['DateTime']['output'];
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
Expand Down Expand Up @@ -1583,6 +1590,7 @@ export type Workspace = {
displayName?: Maybe<Scalars['String']['output']>;
domainName?: Maybe<Scalars['String']['output']>;
featureFlags?: Maybe<Array<FeatureFlag>>;
hasValidEntrepriseKey: Scalars['Boolean']['output'];
id: Scalars['UUID']['output'];
inviteHash?: Maybe<Scalars['String']['output']>;
isPublicInviteLinkEnabled: Scalars['Boolean']['output'];
Expand Down
25 changes: 18 additions & 7 deletions packages/twenty-front/src/generated/graphql.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip';
import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
import { useGraphData } from '@/analytics/hooks/useGraphData';

import { WebhookAnalyticsTooltip } from '@/analytics/components/WebhookAnalyticsTooltip';
import { useAnalyticsGraphDataState } from '@/analytics/hooks/useAnalyticsGraphDataState';
import { Select } from '@/ui/input/components/Select';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
Expand All @@ -9,15 +10,8 @@ import { Section } from '@react-email/components';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { H2Title } from 'twenty-ui';
import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';

export type NivoLineInput = {
id: string | number;
color?: string;
data: Array<{
x: number | string | Date;
y: number | string | Date;
}>;
};
const StyledGraphContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
Expand All @@ -33,34 +27,44 @@ const StyledTitleContainer = styled.div`
justify-content: space-between;
`;

type SettingsDevelopersWebhookUsageGraphProps = {
webhookId: string;
type AnalyticsActivityGraphProps = {
recordId: string;
recordType: string;
endpointName: keyof AnalyticsTinybirdJwtMap;
};

export const SettingsDevelopersWebhookUsageGraph = ({
webhookId,
}: SettingsDevelopersWebhookUsageGraphProps) => {
const webhookGraphData = useRecoilValue(webhookGraphDataState);
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
export const AnalyticsActivityGraph = ({
recordId,
recordType,
endpointName,
}: AnalyticsActivityGraphProps) => {
const { analyticsState, transformDataFunction } =
useAnalyticsGraphDataState(endpointName);
const analytics = useRecoilValue(analyticsState);
const setAnalyticsGraphData = useSetRecoilState(analyticsState);
const theme = useTheme();

const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
'7D' | '1D' | '12H' | '4H'
>('7D');

const { fetchGraphData } = useGraphData(webhookId);
const { fetchGraphData } = useGraphData({
recordId,
recordType,
endpointName,
});

return (
<>
{webhookGraphData.length ? (
{analytics.length ? (
<Section>
<StyledTitleContainer>
<H2Title
title="Activity"
description="See your webhook activity over time"
description={`See your ${recordType} activity over time`}
/>
<Select
dropdownId="test-id-webhook-graph"
dropdownId={`test-id-${endpointName}-graph`}
value={windowLengthGraphOption}
options={[
{ value: '7D', label: 'This week' },
Expand All @@ -71,15 +75,15 @@ export const SettingsDevelopersWebhookUsageGraph = ({
onChange={(windowLengthGraphOption) => {
setWindowLengthGraphOption(windowLengthGraphOption);
fetchGraphData(windowLengthGraphOption).then((graphInput) => {
setWebhookGraphData(graphInput);
setAnalyticsGraphData(transformDataFunction(graphInput));
});
}}
/>
</StyledTitleContainer>

<StyledGraphContainer>
<ResponsiveLine
data={webhookGraphData}
data={analytics}
curve={'monotoneX'}
enableArea={true}
colors={(d) => d.color}
Expand Down Expand Up @@ -167,9 +171,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
useMesh={true}
enableSlices={false}
enableCrosshair={false}
tooltip={({ point }) => (
<SettingsDevelopersWebhookTooltip point={point} />
)}
tooltip={({ point }) => <WebhookAnalyticsTooltip point={point} />} // later add a condition to get different tooltips
/>
</StyledGraphContainer>
</Section>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useAnalyticsGraphDataState } from '@/analytics/hooks/useAnalyticsGraphDataState';
import { useGraphData } from '@/analytics/hooks/useGraphData';

import { useEffect, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';

type AnalyticsGraphEffectProps = {
recordId: string;
recordType: string;
endpointName: keyof AnalyticsTinybirdJwtMap;
};

export const AnalyticsGraphEffect = ({
recordId,
recordType,
endpointName,
}: AnalyticsGraphEffectProps) => {
const { analyticsState, transformDataFunction } =
useAnalyticsGraphDataState(endpointName);
const setGraphData = useSetRecoilState(analyticsState);
const [isLoaded, setIsLoaded] = useState(false);

const { fetchGraphData } = useGraphData({
recordId,
recordType,
endpointName,
});

useEffect(() => {
if (!isLoaded) {
fetchGraphData('7D').then((graphInput) => {
setGraphData(transformDataFunction(graphInput));
});
setIsLoaded(true);
}
}, [
fetchGraphData,
isLoaded,
setGraphData,
recordId,
recordType,
endpointName,
transformDataFunction,
]);

return <></>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ const StyledDataDefinition = styled.div`
const StyledSpan = styled.span`
color: ${({ theme }) => theme.font.color.primary};
`;
type SettingsDevelopersWebhookTooltipProps = {
type WebhookAnalyticsTooltipProps = {
point: Point;
};
export const SettingsDevelopersWebhookTooltip = ({
export const WebhookAnalyticsTooltip = ({
point,
}: SettingsDevelopersWebhookTooltipProps): ReactElement => {
}: WebhookAnalyticsTooltipProps): ReactElement => {
const { timeFormat, timeZone } = useContext(UserContext);
const windowInterval = new Date(point.data.x);
const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const ANALYTICS_GRAPH_OPTION_MAP = {
'7D': { granularity: 'day' },
'1D': { granularity: 'hour' },
'12H': { granularity: 'hour' },
'4H': { granularity: 'hour' },
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//Use this map when granularity is custom meanwhile i'll use the first map
export const ANALYTICS_GRAPH_OPTION_MAP_CUSTOM = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this? Or can we remove it before merging?

'7D': { granularity: 'custom', tickIntervalInMinutes: '420' },
'1D': { granularity: 'custom', tickIntervalInMinutes: '60' },
'12H': { granularity: 'custom', tickIntervalInMinutes: '30' },
'4H': { granularity: 'custom', tickIntervalInMinutes: '10' },
};
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
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: '[email protected]',
canImpersonate: false,
userVars: {},
analyticsTinybirdJwts: {
getPageviewsAnalytics: TEST_JWT_TOKEN,
},
} as CurrentUser);
});

expect(result.current.hook).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { webhookAnalyticsGraphDataState } from '@/analytics/states/webhookAnalyticsGraphDataState';

import { mapWebhookAnalyticsResultToNivoLineInput } from '@/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput';

export const useAnalyticsGraphDataState = (endpointName: string) => {
switch (endpointName) {
case 'getWebhookAnalytics':
return {
analyticsState: webhookAnalyticsGraphDataState,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like what you want to use here might be componentState? Look for createComponentState in the code :)

transformDataFunction: mapWebhookAnalyticsResultToNivoLineInput,
};
default:
throw new Error(
`No analytics state associated with endpoint "${endpointName}"`,
);
}
};
Original file line number Diff line number Diff line change
@@ -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];
};
49 changes: 49 additions & 0 deletions packages/twenty-front/src/modules/analytics/hooks/useGraphData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useAnalyticsTinybirdJwts } from '@/analytics/hooks/useAnalyticsTinybirdJwts';
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 { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';

export const useGraphData = ({
recordType,
recordId,
endpointName,
}: {
recordType: string;
recordId: string;
endpointName: keyof AnalyticsTinybirdJwtMap;
}) => {
const { enqueueSnackBar } = useSnackBar();
const tinybirdJwt = useAnalyticsTinybirdJwts(endpointName);

const fetchGraphData = async (
windowLengthGraphOption: '7D' | '1D' | '12H' | '4H',
) => {
try {
if (isUndefined(tinybirdJwt)) {
throw new Error('No jwt associated with this endpoint found');
}

return await fetchGraphDataOrThrow({
recordId,
recordType,
windowLength: windowLengthGraphOption,
tinybirdJwt,
endpointName,
});
} catch (error) {
if (error instanceof Error) {
enqueueSnackBar(
`Something went wrong while fetching webhook usage: ${error.message}`,
{
variant: SnackBarVariant.Error,
},
);
}
return [];
}
};
return { fetchGraphData };
};
Loading
Loading