-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Changes from 11 commits
35a64da
2adc9ed
d8c8350
acfb7c7
2e44eaa
9bea5a0
214a7da
248250a
b3ee38e
1c674ba
9e299b4
e4ddcae
7cbeec7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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}; | ||
|
@@ -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 ( | ||
<> | ||
{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`} | ||
/> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' }, | ||
|
@@ -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, | ||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO |
||
tickPadding: 5, | ||
tickSize: 6, | ||
|
@@ -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> | ||
|
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 |
---|---|---|
@@ -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(); | ||
}); | ||
}); |
There was a problem hiding this comment.
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?