Add webhook response graph from the last 5 days (#7487)
#7346 #7343 #7342 #7344 Before: <img width="799" alt="Screenshot 2024-10-08 at 11 59 37" src="https://github.com/user-attachments/assets/a1cd1714-41ed-4f96-85eb-2861e7a8b2c2"> Now:  In order to test: 1. Set ANALYTICS_ENABLED to true 2. Set TINYBIRD_TOKEN to your token from the workspace _twenty_analytics_playground_ 3. Write your client tinybird token in SettingsDeveloppersWebhookDetail.tsx in line 93 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.
This commit is contained in:
committed by
GitHub
parent
798722179e
commit
f901512a4f
@ -30,6 +30,9 @@
|
||||
"workerDirectory": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nivo/calendar": "^0.87.0",
|
||||
"@nivo/core": "^0.87.0",
|
||||
"@nivo/line": "^0.87.0",
|
||||
"@xyflow/react": "^12.0.4",
|
||||
"transliteration": "^2.3.5"
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ const SettingsDevelopersApiKeysNew = lazy(() =>
|
||||
|
||||
const SettingsDevelopersWebhooksNew = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew'
|
||||
'~/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew'
|
||||
).then((module) => ({
|
||||
default: module.SettingsDevelopersWebhooksNew,
|
||||
})),
|
||||
@ -165,7 +165,7 @@ const SettingsObjects = lazy(() =>
|
||||
|
||||
const SettingsDevelopersWebhooksDetail = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/developers/webhooks/SettingsDevelopersWebhookDetail'
|
||||
'~/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail'
|
||||
).then((module) => ({
|
||||
default: module.SettingsDevelopersWebhooksDetail,
|
||||
})),
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
|
||||
import styled from '@emotion/styled';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
import { Section } from '@react-email/components';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
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`
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
`;
|
||||
export const SettingsDeveloppersWebhookUsageGraph = () => {
|
||||
const webhookGraphData = useRecoilValue(webhookGraphDataState);
|
||||
|
||||
return (
|
||||
<>
|
||||
{webhookGraphData.length ? (
|
||||
<Section>
|
||||
<H2Title title="Statistics" />
|
||||
<StyledGraphContainer>
|
||||
<ResponsiveLine
|
||||
data={webhookGraphData}
|
||||
colors={(d) => d.color}
|
||||
margin={{ top: 0, right: 0, bottom: 50, left: 60 }}
|
||||
xFormat="time:%Y-%m-%d %H:%M%"
|
||||
xScale={{
|
||||
type: 'time',
|
||||
useUTC: false,
|
||||
format: '%Y-%m-%d %H:%M:%S',
|
||||
precision: 'hour',
|
||||
}}
|
||||
yScale={{
|
||||
type: 'linear',
|
||||
}}
|
||||
axisBottom={{
|
||||
tickValues: 'every day',
|
||||
format: '%b %d',
|
||||
}}
|
||||
enableTouchCrosshair={true}
|
||||
enableGridY={false}
|
||||
enableGridX={false}
|
||||
enablePoints={false}
|
||||
/>
|
||||
</StyledGraphContainer>
|
||||
</Section>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,101 @@
|
||||
import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
|
||||
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
type SettingsDevelopersWebhookUsageGraphEffectProps = {
|
||||
webhookId: string;
|
||||
};
|
||||
|
||||
export const SettingsDevelopersWebhookUsageGraphEffect = ({
|
||||
webhookId,
|
||||
}: SettingsDevelopersWebhookUsageGraphEffectProps) => {
|
||||
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const queryString = new URLSearchParams({
|
||||
webhookIdRequest: webhookId,
|
||||
}).toString();
|
||||
const token = 'REPLACE_ME';
|
||||
const response = await fetch(
|
||||
`https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalytics.json?${queryString}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token,
|
||||
},
|
||||
},
|
||||
);
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
enqueueSnackBar('Something went wrong while fetching webhook usage', {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const graphInput = result.data
|
||||
.flatMap(
|
||||
(dataRow: {
|
||||
start_interval: string;
|
||||
failure_count: number;
|
||||
success_count: number;
|
||||
}) => [
|
||||
{
|
||||
x: dataRow.start_interval,
|
||||
y: dataRow.failure_count,
|
||||
id: 'failure_count',
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
x: dataRow.start_interval,
|
||||
y: dataRow.success_count,
|
||||
id: 'success_count',
|
||||
color: 'green',
|
||||
},
|
||||
],
|
||||
)
|
||||
.reduce(
|
||||
(
|
||||
acc: NivoLineInput[],
|
||||
{
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
color,
|
||||
}: { id: string; x: string; y: number; color: string },
|
||||
) => {
|
||||
const existingGroupIndex = acc.findIndex(
|
||||
(group) => group.id === id,
|
||||
);
|
||||
const isExistingGroup = existingGroupIndex !== -1;
|
||||
|
||||
if (isExistingGroup) {
|
||||
return acc.map((group, index) =>
|
||||
index === existingGroupIndex
|
||||
? { ...group, data: [...group.data, { x, y }] }
|
||||
: group,
|
||||
);
|
||||
} else {
|
||||
return [...acc, { id, color, data: [{ x, y }] }];
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
setWebhookGraphData(graphInput);
|
||||
} catch (error) {
|
||||
enqueueSnackBar('Something went wrong while fetching webhook usage', {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [enqueueSnackBar, setWebhookGraphData, webhookId]);
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const webhookGraphDataState = createState<NivoLineInput[]>({
|
||||
key: 'webhookGraphData',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -13,4 +13,5 @@ export type FeatureFlagKey =
|
||||
| 'IS_SEARCH_ENABLED'
|
||||
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
|
||||
| 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED'
|
||||
| 'IS_WORKSPACE_MIGRATED_FOR_SEARCH';
|
||||
| 'IS_WORKSPACE_MIGRATED_FOR_SEARCH'
|
||||
| 'IS_ANALYTICS_V2_ENABLED';
|
||||
|
||||
@ -2,7 +2,7 @@ import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/test';
|
||||
import { graphql, HttpResponse } from 'msw';
|
||||
|
||||
import { SettingsDevelopersWebhooksDetail } from '~/pages/settings/developers/webhooks/SettingsDevelopersWebhookDetail';
|
||||
import { SettingsDevelopersWebhooksDetail } from '~/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail';
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/test';
|
||||
|
||||
import { SettingsDevelopersWebhooksNew } from '~/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew';
|
||||
import { SettingsDevelopersWebhooksNew } from '~/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew';
|
||||
import {
|
||||
PageDecorator,
|
||||
PageDecoratorArgs,
|
||||
|
||||
@ -11,6 +11,8 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
||||
import { SettingsDeveloppersWebhookUsageGraph } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
|
||||
import { SettingsDevelopersWebhookUsageGraphEffect } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
@ -20,6 +22,7 @@ import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
const StyledFilterRow = styled.div`
|
||||
display: flex;
|
||||
@ -63,6 +66,8 @@ export const SettingsDevelopersWebhooksDetail = () => {
|
||||
navigate(developerPath);
|
||||
};
|
||||
|
||||
const isAnalyticsV2Enabled = useIsFeatureEnabled('IS_ANALYTICS_V2_ENABLED');
|
||||
|
||||
const fieldTypeOptions = [
|
||||
{ value: '*', label: 'All Objects' },
|
||||
...objectMetadataItems.map((item) => ({
|
||||
@ -173,6 +178,14 @@ export const SettingsDevelopersWebhooksDetail = () => {
|
||||
/>
|
||||
</StyledFilterRow>
|
||||
</Section>
|
||||
{isAnalyticsV2Enabled ? (
|
||||
<>
|
||||
<SettingsDevelopersWebhookUsageGraphEffect webhookId={webhookId} />
|
||||
<SettingsDeveloppersWebhookUsageGraph />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Delete this integration" />
|
||||
<Button
|
||||
@ -70,6 +70,11 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IsAnalyticsV2Enabled,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IsGmailSendEmailScopeEnabled,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
@ -18,17 +19,41 @@ export type CallWebhookJobData = {
|
||||
@Processor(MessageQueue.webhookQueue)
|
||||
export class CallWebhookJob {
|
||||
private readonly logger = new Logger(CallWebhookJob.name);
|
||||
|
||||
constructor(private readonly httpService: HttpService) {}
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
) {}
|
||||
|
||||
@Process(CallWebhookJob.name)
|
||||
async handle(data: CallWebhookJobData): Promise<void> {
|
||||
try {
|
||||
await this.httpService.axiosRef.post(data.targetUrl, data);
|
||||
this.logger.log(
|
||||
`CallWebhookJob successfully called on targetUrl '${data.targetUrl}'`,
|
||||
const response = await this.httpService.axiosRef.post(
|
||||
data.targetUrl,
|
||||
data,
|
||||
);
|
||||
const eventInput = {
|
||||
action: 'webhook.response',
|
||||
payload: {
|
||||
status: response.status,
|
||||
url: data.targetUrl,
|
||||
webhookId: data.webhookId,
|
||||
eventName: data.eventName,
|
||||
},
|
||||
};
|
||||
|
||||
this.analyticsService.create(eventInput, 'webhook', data.workspaceId);
|
||||
} catch (err) {
|
||||
const eventInput = {
|
||||
action: 'webhook.response',
|
||||
payload: {
|
||||
status: err.response.status,
|
||||
url: data.targetUrl,
|
||||
webhookId: data.webhookId,
|
||||
eventName: data.eventName,
|
||||
},
|
||||
};
|
||||
|
||||
this.analyticsService.create(eventInput, 'webhook', data.workspaceId);
|
||||
this.logger.error(
|
||||
`Error calling webhook on targetUrl '${data.targetUrl}': ${err}`,
|
||||
);
|
||||
|
||||
@ -5,6 +5,7 @@ import { CallWebhookJobsJob } from 'src/engine/api/graphql/workspace-query-runne
|
||||
import { CallWebhookJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job';
|
||||
import { RecordPositionBackfillJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/record-position-backfill.job';
|
||||
import { RecordPositionBackfillModule } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module';
|
||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@ -14,6 +15,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
|
||||
DataSourceModule,
|
||||
RecordPositionBackfillModule,
|
||||
HttpModule,
|
||||
AnalyticsModule,
|
||||
],
|
||||
providers: [CallWebhookJobsJob, CallWebhookJob, RecordPositionBackfillJob],
|
||||
})
|
||||
|
||||
@ -13,4 +13,5 @@ export enum FeatureFlagKey {
|
||||
IsSearchEnabled = 'IS_SEARCH_ENABLED',
|
||||
IsWorkspaceMigratedForSearch = 'IS_WORKSPACE_MIGRATED_FOR_SEARCH',
|
||||
IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED',
|
||||
IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user