Skip to content

Commit

Permalink
refactor webhookAnalytics call and enrich analytics module (#8253)
Browse files Browse the repository at this point in the history
**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 <[email protected]>
  • Loading branch information
anamarn and FelixMalfait authored Nov 8, 2024
1 parent f9c076d commit f06cdbd
Show file tree
Hide file tree
Showing 62 changed files with 1,429 additions and 539 deletions.
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_GRAPH_DESCRIPTION_MAP } from '@/analytics/constants/AnalyticsGraphDescriptionMap';
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,38 @@ 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();
return (
<>
{webhookGraphData.length ? (
{analyticsGraphData.length ? (
<Section>
<StyledTitleContainer>
<H2Title
title="Activity"
description="See your webhook activity over time"
title={`${ANALYTICS_GRAPH_TITLE_MAP[endpointName]}`}
description={`${ANALYTICS_GRAPH_DESCRIPTION_MAP[endpointName]}`}
/>
<Select
dropdownId="test-id-webhook-graph"
dropdownId={dropdownId}
value={windowLengthGraphOption}
options={[
{ value: '7D', label: 'This week' },
Expand All @@ -71,18 +71,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 +151,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
type: 'linear',
}}
axisBottom={{
format: '%b %d, %I:%M %p',
format: '%b %d, %I:%M %p', //TODO: add the user prefered time format for the graph
tickValues: 2,
tickPadding: 5,
tickSize: 6,
Expand All @@ -167,9 +169,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,10 @@
import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';

export const ANALYTICS_GRAPH_DESCRIPTION_MAP: AnalyticsTinybirdJwtMap = {
getWebhookAnalytics: 'See your webhook activity over time',
getPageviewsAnalytics: 'See your Page Views activity over time',
getUsersAnalytics: 'See your Users activity over time',
getServerlessFunctionDuration: 'See your function duration over time',
getServerlessFunctionSuccessRate: 'See your function success rate over time',
getServerlessFunctionErrorCount: 'See your function error count over time',
};
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

0 comments on commit f06cdbd

Please sign in to comment.