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
12 changes: 11 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,16 @@ export type Analytics = {
success: Scalars['Boolean']['output'];
};

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

export type ApiConfig = {
__typename?: 'ApiConfig';
mutationMaximumAffectedRecords: Scalars['Float']['output'];
Expand Down Expand Up @@ -1497,7 +1507,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
29 changes: 23 additions & 6 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,23 +1,19 @@
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 { WebhookAnalyticsTooltip } from '@/analytics/components/WebhookAnalyticsTooltip';
import { ANALYTICS_ENDPOINT_TYPE_MAP } from '@/analytics/constants/AnalyticsEndpointTypeMap';
import { ANALYTICS_GRAPH_TITLE_MAP } from '@/analytics/constants/AnalyticsGraphTitleMap';
import { useGraphData } from '@/analytics/hooks/useGraphData';
import { analyticsGraphDataComponentState } from '@/analytics/states/analyticsGraphDataComponentState';
import { AnalyticsComponentProps as AnalyticsActivityGraphProps } from '@/analytics/types/AnalyticsComponentProps';
import { computeAnalyticsGraphDataFunction } from '@/analytics/utils/computeAnalyticsGraphDataFunction';
import { Select } from '@/ui/input/components/Select';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ResponsiveLine } from '@nivo/line';
import { Section } from '@react-email/components';
import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useId, useState } from 'react';
import { H2Title } from 'twenty-ui';

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 +29,39 @@ const StyledTitleContainer = styled.div`
justify-content: space-between;
`;

type SettingsDevelopersWebhookUsageGraphProps = {
webhookId: string;
};

export const SettingsDevelopersWebhookUsageGraph = ({
webhookId,
}: SettingsDevelopersWebhookUsageGraphProps) => {
const webhookGraphData = useRecoilValue(webhookGraphDataState);
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
export const AnalyticsActivityGraph = ({
recordId,
endpointName,
}: AnalyticsActivityGraphProps) => {
const [analyticsGraphData, setAnalyticsGraphData] = useRecoilComponentStateV2(
analyticsGraphDataComponentState,
);
const theme = useTheme();

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

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

const transformDataFunction = computeAnalyticsGraphDataFunction(endpointName);

const dropdownId = useId();
// perhaps here i need to separate the Section container and the graph itself? TODO: Add elements of distintion btwen graphs of the same record type
return (
Copy link
Member

Choose a reason for hiding this comment

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

Could you remove this comment?

<>
{webhookGraphData.length ? (
{analyticsGraphData.length ? (
<Section>
<StyledTitleContainer>
<H2Title
title="Activity"
description="See your webhook activity over time"
title={`${ANALYTICS_GRAPH_TITLE_MAP[endpointName]}`}
description={`See your ${ANALYTICS_ENDPOINT_TYPE_MAP[endpointName]} activity over time`}
/>
Copy link
Member

Choose a reason for hiding this comment

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

Let's stay consistent and use a map for descriptions, just like you did for title. This will give us more flexibility to customize the text. (note we could have also passed title/description as parameter of the component, that's how I would have done it intuitively but the way you did it works too!)

<Select
dropdownId="test-id-webhook-graph"
dropdownId={dropdownId}
value={windowLengthGraphOption}
options={[
{ value: '7D', label: 'This week' },
Expand All @@ -71,18 +72,20 @@ export const SettingsDevelopersWebhookUsageGraph = ({
onChange={(windowLengthGraphOption) => {
setWindowLengthGraphOption(windowLengthGraphOption);
fetchGraphData(windowLengthGraphOption).then((graphInput) => {
setWebhookGraphData(graphInput);
setAnalyticsGraphData(transformDataFunction(graphInput));
});
}}
/>
</StyledTitleContainer>

<StyledGraphContainer>
<ResponsiveLine
data={webhookGraphData}
data={analyticsGraphData}
curve={'monotoneX'}
enableArea={true}
colors={(d) => d.color}
colors={{ scheme: 'set1' }}
//it "addapts" to the color scheme of the graph without hardcoding them
//is there a color scheme for graph Data in twenty? Do we always want the gradient?
theme={{
text: {
fill: theme.font.color.light,
Expand Down Expand Up @@ -149,7 +152,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
type: 'linear',
}}
axisBottom={{
format: '%b %d, %I:%M %p',
format: '%b %d, %I:%M %p', //TDO: add the user prefered time format for the graph
tickValues: 2,
Copy link
Member

Choose a reason for hiding this comment

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

TODO

tickPadding: 5,
tickSize: 6,
Expand All @@ -167,9 +170,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,32 @@
import { useGraphData } from '@/analytics/hooks/useGraphData';
import { analyticsGraphDataComponentState } from '@/analytics/states/analyticsGraphDataComponentState';
import { AnalyticsComponentProps as AnalyticsGraphEffectProps } from '@/analytics/types/AnalyticsComponentProps';
import { computeAnalyticsGraphDataFunction } from '@/analytics/utils/computeAnalyticsGraphDataFunction';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useState } from 'react';

export const AnalyticsGraphEffect = ({
recordId,
endpointName,
}: AnalyticsGraphEffectProps) => {
const setAnalyticsGraphData = useSetRecoilComponentStateV2(
analyticsGraphDataComponentState,
);

const transformDataFunction = computeAnalyticsGraphDataFunction(endpointName);
const [isLoaded, setIsLoaded] = useState(false);

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

if (!isLoaded) {
fetchGraphData('7D').then((graphInput) => {
setAnalyticsGraphData(transformDataFunction(graphInput));
});
setIsLoaded(true);
}

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,10 @@
import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';

export const ANALYTICS_ENDPOINT_TYPE_MAP: AnalyticsTinybirdJwtMap = {
getWebhookAnalytics: 'webhook',
getPageviewsAnalytics: 'pageviews',
getUsersAnalytics: 'users',
getServerlessFunctionDuration: 'function',
getServerlessFunctionSuccessRate: 'function',
getServerlessFunctionErrorCount: 'function',
};
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,10 @@
import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';

export const ANALYTICS_GRAPH_TITLE_MAP: AnalyticsTinybirdJwtMap = {
getWebhookAnalytics: 'Activity',
getPageviewsAnalytics: 'Page Views',
getUsersAnalytics: 'Users',
getServerlessFunctionDuration: 'Duration (ms)',
getServerlessFunctionSuccessRate: 'Success Rate (%)',
getServerlessFunctionErrorCount: 'Error Count',
};
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();
});
});
Loading
Loading