refactor webhookAnalytics call and enrich analytics module (#8253)
**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 <felix@twenty.com>
This commit is contained in:
committed by
GitHub
parent
f9c076df31
commit
f06cdbdfc6
@ -38,6 +38,16 @@ export type Analytics = {
|
|||||||
success: Scalars['Boolean']['output'];
|
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 = {
|
export type ApiConfig = {
|
||||||
__typename?: 'ApiConfig';
|
__typename?: 'ApiConfig';
|
||||||
mutationMaximumAffectedRecords: Scalars['Float']['output'];
|
mutationMaximumAffectedRecords: Scalars['Float']['output'];
|
||||||
@ -1497,7 +1507,7 @@ export type UpdateWorkspaceInput = {
|
|||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
__typename?: 'User';
|
__typename?: 'User';
|
||||||
analyticsTinybirdJwt?: Maybe<Scalars['String']['output']>;
|
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
||||||
canImpersonate: Scalars['Boolean']['output'];
|
canImpersonate: Scalars['Boolean']['output'];
|
||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
|
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
|
||||||
|
|||||||
@ -31,6 +31,16 @@ export type Analytics = {
|
|||||||
success: Scalars['Boolean'];
|
success: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AnalyticsTinybirdJwtMap = {
|
||||||
|
__typename?: 'AnalyticsTinybirdJwtMap';
|
||||||
|
getPageviewsAnalytics: Scalars['String'];
|
||||||
|
getServerlessFunctionDuration: Scalars['String'];
|
||||||
|
getServerlessFunctionErrorCount: Scalars['String'];
|
||||||
|
getServerlessFunctionSuccessRate: Scalars['String'];
|
||||||
|
getUsersAnalytics: Scalars['String'];
|
||||||
|
getWebhookAnalytics: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiConfig = {
|
export type ApiConfig = {
|
||||||
__typename?: 'ApiConfig';
|
__typename?: 'ApiConfig';
|
||||||
mutationMaximumAffectedRecords: Scalars['Float'];
|
mutationMaximumAffectedRecords: Scalars['Float'];
|
||||||
@ -1207,7 +1217,7 @@ export type UpdateWorkspaceInput = {
|
|||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
__typename?: 'User';
|
__typename?: 'User';
|
||||||
analyticsTinybirdJwt?: Maybe<Scalars['String']>;
|
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
|
||||||
canImpersonate: Scalars['Boolean'];
|
canImpersonate: Scalars['Boolean'];
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
defaultAvatarUrl?: Maybe<Scalars['String']>;
|
defaultAvatarUrl?: Maybe<Scalars['String']>;
|
||||||
@ -1696,7 +1706,7 @@ export type ImpersonateMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type RenewTokenMutationVariables = Exact<{
|
export type RenewTokenMutationVariables = Exact<{
|
||||||
appToken: Scalars['String'];
|
appToken: Scalars['String'];
|
||||||
@ -1729,7 +1739,7 @@ export type VerifyMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type CheckUserExistsQueryVariables = Exact<{
|
export type CheckUserExistsQueryVariables = Exact<{
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@ -1816,7 +1826,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key:
|
|||||||
|
|
||||||
export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdpType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
|
export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdpType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
|
||||||
|
|
||||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
||||||
|
|
||||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -1833,7 +1843,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
|||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
||||||
|
|
||||||
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||||
workflowVersionId: Scalars['String'];
|
workflowVersionId: Scalars['String'];
|
||||||
@ -2060,7 +2070,14 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
email
|
email
|
||||||
canImpersonate
|
canImpersonate
|
||||||
supportUserHash
|
supportUserHash
|
||||||
analyticsTinybirdJwt
|
analyticsTinybirdJwts {
|
||||||
|
getWebhookAnalytics
|
||||||
|
getPageviewsAnalytics
|
||||||
|
getUsersAnalytics
|
||||||
|
getServerlessFunctionDuration
|
||||||
|
getServerlessFunctionSuccessRate
|
||||||
|
getServerlessFunctionErrorCount
|
||||||
|
}
|
||||||
onboardingStatus
|
onboardingStatus
|
||||||
workspaceMember {
|
workspaceMember {
|
||||||
...WorkspaceMemberQueryFragment
|
...WorkspaceMemberQueryFragment
|
||||||
|
|||||||
@ -1,23 +1,19 @@
|
|||||||
import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip';
|
import { WebhookAnalyticsTooltip } from '@/analytics/components/WebhookAnalyticsTooltip';
|
||||||
import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
|
import { ANALYTICS_GRAPH_DESCRIPTION_MAP } from '@/analytics/constants/AnalyticsGraphDescriptionMap';
|
||||||
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
|
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 { Select } from '@/ui/input/components/Select';
|
||||||
|
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { ResponsiveLine } from '@nivo/line';
|
import { ResponsiveLine } from '@nivo/line';
|
||||||
import { Section } from '@react-email/components';
|
import { Section } from '@react-email/components';
|
||||||
import { useState } from 'react';
|
import { useId, useState } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import { H2Title } from 'twenty-ui';
|
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`
|
const StyledGraphContainer = styled.div`
|
||||||
background-color: ${({ theme }) => theme.background.secondary};
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
@ -33,34 +29,38 @@ const StyledTitleContainer = styled.div`
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type SettingsDevelopersWebhookUsageGraphProps = {
|
export const AnalyticsActivityGraph = ({
|
||||||
webhookId: string;
|
recordId,
|
||||||
};
|
endpointName,
|
||||||
|
}: AnalyticsActivityGraphProps) => {
|
||||||
export const SettingsDevelopersWebhookUsageGraph = ({
|
const [analyticsGraphData, setAnalyticsGraphData] = useRecoilComponentStateV2(
|
||||||
webhookId,
|
analyticsGraphDataComponentState,
|
||||||
}: SettingsDevelopersWebhookUsageGraphProps) => {
|
);
|
||||||
const webhookGraphData = useRecoilValue(webhookGraphDataState);
|
|
||||||
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
|
const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
|
||||||
'7D' | '1D' | '12H' | '4H'
|
'7D' | '1D' | '12H' | '4H'
|
||||||
>('7D');
|
>('7D');
|
||||||
|
|
||||||
const { fetchGraphData } = useGraphData(webhookId);
|
const { fetchGraphData } = useGraphData({
|
||||||
|
recordId,
|
||||||
|
endpointName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const transformDataFunction = computeAnalyticsGraphDataFunction(endpointName);
|
||||||
|
|
||||||
|
const dropdownId = useId();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{webhookGraphData.length ? (
|
{analyticsGraphData.length ? (
|
||||||
<Section>
|
<Section>
|
||||||
<StyledTitleContainer>
|
<StyledTitleContainer>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Activity"
|
title={`${ANALYTICS_GRAPH_TITLE_MAP[endpointName]}`}
|
||||||
description="See your webhook activity over time"
|
description={`${ANALYTICS_GRAPH_DESCRIPTION_MAP[endpointName]}`}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
dropdownId="test-id-webhook-graph"
|
dropdownId={dropdownId}
|
||||||
value={windowLengthGraphOption}
|
value={windowLengthGraphOption}
|
||||||
options={[
|
options={[
|
||||||
{ value: '7D', label: 'This week' },
|
{ value: '7D', label: 'This week' },
|
||||||
@ -71,7 +71,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
|||||||
onChange={(windowLengthGraphOption) => {
|
onChange={(windowLengthGraphOption) => {
|
||||||
setWindowLengthGraphOption(windowLengthGraphOption);
|
setWindowLengthGraphOption(windowLengthGraphOption);
|
||||||
fetchGraphData(windowLengthGraphOption).then((graphInput) => {
|
fetchGraphData(windowLengthGraphOption).then((graphInput) => {
|
||||||
setWebhookGraphData(graphInput);
|
setAnalyticsGraphData(transformDataFunction(graphInput));
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -79,10 +79,12 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
|||||||
|
|
||||||
<StyledGraphContainer>
|
<StyledGraphContainer>
|
||||||
<ResponsiveLine
|
<ResponsiveLine
|
||||||
data={webhookGraphData}
|
data={analyticsGraphData}
|
||||||
curve={'monotoneX'}
|
curve={'monotoneX'}
|
||||||
enableArea={true}
|
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={{
|
theme={{
|
||||||
text: {
|
text: {
|
||||||
fill: theme.font.color.light,
|
fill: theme.font.color.light,
|
||||||
@ -149,7 +151,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
|||||||
type: 'linear',
|
type: 'linear',
|
||||||
}}
|
}}
|
||||||
axisBottom={{
|
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,
|
tickValues: 2,
|
||||||
tickPadding: 5,
|
tickPadding: 5,
|
||||||
tickSize: 6,
|
tickSize: 6,
|
||||||
@ -167,9 +169,7 @@ export const SettingsDevelopersWebhookUsageGraph = ({
|
|||||||
useMesh={true}
|
useMesh={true}
|
||||||
enableSlices={false}
|
enableSlices={false}
|
||||||
enableCrosshair={false}
|
enableCrosshair={false}
|
||||||
tooltip={({ point }) => (
|
tooltip={({ point }) => <WebhookAnalyticsTooltip point={point} />} // later add a condition to get different tooltips
|
||||||
<SettingsDevelopersWebhookTooltip point={point} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</StyledGraphContainer>
|
</StyledGraphContainer>
|
||||||
</Section>
|
</Section>
|
||||||
@ -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 <></>;
|
||||||
|
};
|
||||||
@ -58,12 +58,12 @@ const StyledDataDefinition = styled.div`
|
|||||||
const StyledSpan = styled.span`
|
const StyledSpan = styled.span`
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
`;
|
`;
|
||||||
type SettingsDevelopersWebhookTooltipProps = {
|
type WebhookAnalyticsTooltipProps = {
|
||||||
point: Point;
|
point: Point;
|
||||||
};
|
};
|
||||||
export const SettingsDevelopersWebhookTooltip = ({
|
export const WebhookAnalyticsTooltip = ({
|
||||||
point,
|
point,
|
||||||
}: SettingsDevelopersWebhookTooltipProps): ReactElement => {
|
}: WebhookAnalyticsTooltipProps): ReactElement => {
|
||||||
const { timeFormat, timeZone } = useContext(UserContext);
|
const { timeFormat, timeZone } = useContext(UserContext);
|
||||||
const windowInterval = new Date(point.data.x);
|
const windowInterval = new Date(point.data.x);
|
||||||
const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
|
const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
|
||||||
@ -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',
|
||||||
|
};
|
||||||
@ -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',
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
export const ANALYTICS_GRAPH_OPTION_MAP = {
|
||||||
|
'7D': { granularity: 'day' },
|
||||||
|
'1D': { granularity: 'hour' },
|
||||||
|
'12H': { granularity: 'hour' },
|
||||||
|
'4H': { granularity: 'hour' },
|
||||||
|
};
|
||||||
@ -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',
|
||||||
|
};
|
||||||
@ -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: 'test@test.com',
|
||||||
|
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: 'test@test.com',
|
||||||
|
canImpersonate: false,
|
||||||
|
userVars: {},
|
||||||
|
analyticsTinybirdJwts: {
|
||||||
|
getPageviewsAnalytics: TEST_JWT_TOKEN,
|
||||||
|
},
|
||||||
|
} as CurrentUser);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.hook).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
import { useAnalyticsTinybirdJwts } from '@/analytics/hooks/useAnalyticsTinybirdJwts';
|
||||||
|
import { useGraphData } from '@/analytics/hooks/useGraphData';
|
||||||
|
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 { renderHook } from '@testing-library/react';
|
||||||
|
jest.mock('@/analytics/hooks/useAnalyticsTinybirdJwts');
|
||||||
|
jest.mock('@/analytics/utils/fetchGraphDataOrThrow');
|
||||||
|
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
|
||||||
|
|
||||||
|
describe('useGraphData', () => {
|
||||||
|
const mockEnqueueSnackBar = jest.fn();
|
||||||
|
const mockUseSnackBar = jest.fn().mockReturnValue({
|
||||||
|
enqueueSnackBar: mockEnqueueSnackBar,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockUseAnalyticsTinybirdJwts = jest.fn();
|
||||||
|
const mockFetchGraphDataOrThrow = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
(useSnackBar as jest.MockedFunction<typeof useSnackBar>).mockImplementation(
|
||||||
|
mockUseSnackBar,
|
||||||
|
);
|
||||||
|
(
|
||||||
|
useAnalyticsTinybirdJwts as jest.MockedFunction<
|
||||||
|
typeof useAnalyticsTinybirdJwts
|
||||||
|
>
|
||||||
|
).mockImplementation(mockUseAnalyticsTinybirdJwts);
|
||||||
|
(
|
||||||
|
fetchGraphDataOrThrow as jest.MockedFunction<typeof fetchGraphDataOrThrow>
|
||||||
|
).mockImplementation(mockFetchGraphDataOrThrow);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch graph data successfully', async () => {
|
||||||
|
const mockJwt = 'mock-jwt';
|
||||||
|
const mockRecordId = 'mock-record-id';
|
||||||
|
const mockEndpointName = 'getWebhookAnalytics';
|
||||||
|
const mockGraphData = [{ x: '2023-01-01', y: 100 }];
|
||||||
|
|
||||||
|
mockUseAnalyticsTinybirdJwts.mockReturnValue(mockJwt);
|
||||||
|
mockFetchGraphDataOrThrow.mockResolvedValue(mockGraphData);
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGraphData({ recordId: mockRecordId, endpointName: mockEndpointName }),
|
||||||
|
);
|
||||||
|
const { fetchGraphData } = result.current;
|
||||||
|
|
||||||
|
const data = await fetchGraphData('7D');
|
||||||
|
expect(data).toEqual(mockGraphData);
|
||||||
|
expect(mockFetchGraphDataOrThrow).toHaveBeenCalledWith({
|
||||||
|
recordId: mockRecordId,
|
||||||
|
windowLength: '7D',
|
||||||
|
tinybirdJwt: mockJwt,
|
||||||
|
endpointName: mockEndpointName,
|
||||||
|
});
|
||||||
|
expect(mockEnqueueSnackBar).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('should handle errors when fetching graph data', async () => {
|
||||||
|
const mockRecordId = 'mock-record-id';
|
||||||
|
const mockEndpointName = 'getWebhookAnalytics';
|
||||||
|
const mockError = new Error('Something went wrong');
|
||||||
|
|
||||||
|
mockUseAnalyticsTinybirdJwts.mockReturnValue('');
|
||||||
|
mockFetchGraphDataOrThrow.mockRejectedValue(mockError);
|
||||||
|
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGraphData({ recordId: mockRecordId, endpointName: mockEndpointName }),
|
||||||
|
);
|
||||||
|
const { fetchGraphData } = result.current;
|
||||||
|
|
||||||
|
const data = await fetchGraphData('7D');
|
||||||
|
expect(data).toEqual([]);
|
||||||
|
expect(mockFetchGraphDataOrThrow).toHaveBeenCalledWith({
|
||||||
|
recordId: mockRecordId,
|
||||||
|
windowLength: '7D',
|
||||||
|
tinybirdJwt: '',
|
||||||
|
endpointName: mockEndpointName,
|
||||||
|
});
|
||||||
|
expect(mockEnqueueSnackBar).toHaveBeenCalledWith(
|
||||||
|
'Something went wrong while fetching webhook usage: Something went wrong',
|
||||||
|
{
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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];
|
||||||
|
};
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { useAnalyticsTinybirdJwts } from '@/analytics/hooks/useAnalyticsTinybirdJwts';
|
||||||
|
import { AnalyticsComponentProps as useGraphDataProps } from '@/analytics/types/AnalyticsComponentProps';
|
||||||
|
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 { useCallback } from 'react';
|
||||||
|
|
||||||
|
export const useGraphData = ({ recordId, endpointName }: useGraphDataProps) => {
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const tinybirdJwt = useAnalyticsTinybirdJwts(endpointName);
|
||||||
|
|
||||||
|
const fetchGraphData = useCallback(
|
||||||
|
async (windowLengthGraphOption: '7D' | '1D' | '12H' | '4H') => {
|
||||||
|
try {
|
||||||
|
if (isUndefined(tinybirdJwt)) {
|
||||||
|
throw new Error('No jwt associated with this endpoint found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await fetchGraphDataOrThrow({
|
||||||
|
recordId,
|
||||||
|
windowLength: windowLengthGraphOption,
|
||||||
|
tinybirdJwt,
|
||||||
|
endpointName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
enqueueSnackBar(
|
||||||
|
`Something went wrong while fetching webhook usage: ${error.message}`,
|
||||||
|
{
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[tinybirdJwt, recordId, endpointName, enqueueSnackBar],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fetchGraphData };
|
||||||
|
};
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { AnalyticsGraphDataInstanceContext } from '@/analytics/states/contexts/AnalyticsGraphDataInstanceContext';
|
||||||
|
import { NivoLineInput } from '@/analytics/types/NivoLineInput';
|
||||||
|
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||||
|
export const analyticsGraphDataComponentState = createComponentStateV2<
|
||||||
|
NivoLineInput[]
|
||||||
|
>({
|
||||||
|
key: 'analyticsGraphDataComponentState',
|
||||||
|
defaultValue: [],
|
||||||
|
componentInstanceContext: AnalyticsGraphDataInstanceContext,
|
||||||
|
});
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||||
|
|
||||||
|
export const AnalyticsGraphDataInstanceContext =
|
||||||
|
createComponentInstanceContext();
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export type AnalyticsComponentProps = {
|
||||||
|
recordId: string;
|
||||||
|
endpointName: keyof AnalyticsTinybirdJwtMap;
|
||||||
|
};
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
export type NivoLineInput = {
|
||||||
|
id: string | number;
|
||||||
|
color?: string;
|
||||||
|
data: Array<{
|
||||||
|
x: number | string | Date;
|
||||||
|
y: number | string | Date;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { computeAnalyticsGraphDataFunction } from '@/analytics/utils/computeAnalyticsGraphDataFunction';
|
||||||
|
import { mapServerlessFunctionDurationToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionDurationToNivoLineInput';
|
||||||
|
import { mapServerlessFunctionErrorsToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput';
|
||||||
|
import { mapWebhookAnalyticsResultToNivoLineInput } from '@/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput';
|
||||||
|
|
||||||
|
jest.mock('@/analytics/utils/mapServerlessFunctionDurationToNivoLineInput');
|
||||||
|
jest.mock('@/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput');
|
||||||
|
jest.mock('@/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput');
|
||||||
|
|
||||||
|
describe('computeAnalyticsGraphDataFunction', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the mapWebhookAnalyticsResultToNivoLineInput function for "getWebhookAnalytics"', () => {
|
||||||
|
const result = computeAnalyticsGraphDataFunction('getWebhookAnalytics');
|
||||||
|
expect(result).toBe(mapWebhookAnalyticsResultToNivoLineInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the mapServerlessFunctionDurationToNivoLineInput function for "getServerlessFunctionDuration"', () => {
|
||||||
|
const result = computeAnalyticsGraphDataFunction(
|
||||||
|
'getServerlessFunctionDuration',
|
||||||
|
);
|
||||||
|
expect(result).toBe(mapServerlessFunctionDurationToNivoLineInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a function that calls mapServerlessFunctionErrorsToNivoLineInput with "ErrorCount" for "getServerlessFunctionErrorCount"', () => {
|
||||||
|
const result = computeAnalyticsGraphDataFunction(
|
||||||
|
'getServerlessFunctionErrorCount',
|
||||||
|
);
|
||||||
|
const data = [{ start: '2023-01-01', error_count: 10 }];
|
||||||
|
result(data);
|
||||||
|
expect(mapServerlessFunctionErrorsToNivoLineInput).toHaveBeenCalledWith(
|
||||||
|
data,
|
||||||
|
'ErrorCount',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a function that calls mapServerlessFunctionErrorsToNivoLineInput with "SuccessRate" for "getServerlessFunctionSuccessRate"', () => {
|
||||||
|
const result = computeAnalyticsGraphDataFunction(
|
||||||
|
'getServerlessFunctionSuccessRate',
|
||||||
|
);
|
||||||
|
const data = [{ start: '2023-01-01', success_rate: 90 }];
|
||||||
|
result(data);
|
||||||
|
expect(mapServerlessFunctionErrorsToNivoLineInput).toHaveBeenCalledWith(
|
||||||
|
data,
|
||||||
|
'SuccessRate',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error for an unknown endpoint', () => {
|
||||||
|
expect(() => computeAnalyticsGraphDataFunction('unknown')).toThrowError(
|
||||||
|
'No analytics function found associated with endpoint "unknown"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
import { ANALYTICS_GRAPH_OPTION_MAP } from '@/analytics/constants/AnalyticsGraphOptionMap';
|
||||||
|
import { computeStartEndDate } from '@/analytics/utils/computeStartEndDate';
|
||||||
|
import { fetchGraphDataOrThrow } from '@/analytics/utils/fetchGraphDataOrThrow';
|
||||||
|
|
||||||
|
// Im going to make this test more contundent later
|
||||||
|
jest.mock('@/analytics/utils/computeStartEndDate', () => ({
|
||||||
|
computeStartEndDate: jest.fn(() => ({
|
||||||
|
start: '2024-01-01',
|
||||||
|
end: '2024-01-07',
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('fetchGraphDataOrThrow', () => {
|
||||||
|
// Setup fetch mock
|
||||||
|
const mockFetch = jest.fn();
|
||||||
|
global.fetch = mockFetch;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockSuccessResponse = {
|
||||||
|
data: [
|
||||||
|
{ timestamp: '2024-01-01', count: 10 },
|
||||||
|
{ timestamp: '2024-01-02', count: 20 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
recordId: 'test-123',
|
||||||
|
windowLength: '7D',
|
||||||
|
tinybirdJwt: 'test-jwt',
|
||||||
|
endpointName: 'getWebhookAnalytics',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should fetch data successfully for webhook type', async () => {
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockSuccessResponse),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchGraphDataOrThrow(defaultProps);
|
||||||
|
|
||||||
|
// Verify URL construction
|
||||||
|
const lastCallArgs = mockFetch.mock.calls[0][0];
|
||||||
|
expect(lastCallArgs).toContain('webhookId=test-123');
|
||||||
|
expect(lastCallArgs).toContain('getWebhookAnalytics.json');
|
||||||
|
|
||||||
|
// Verify headers
|
||||||
|
const headers = mockFetch.mock.calls[0][1].headers;
|
||||||
|
expect(headers.Authorization).toBe('Bearer test-jwt');
|
||||||
|
|
||||||
|
// Verify response
|
||||||
|
expect(result).toEqual(mockSuccessResponse.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different window lengths correctly', async () => {
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockSuccessResponse),
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchGraphDataOrThrow({
|
||||||
|
...defaultProps,
|
||||||
|
windowLength: '1D',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that correct window length options were used
|
||||||
|
const lastCallArgs = mockFetch.mock.calls[0][0];
|
||||||
|
const options = ANALYTICS_GRAPH_OPTION_MAP['1D'];
|
||||||
|
Object.entries(options).forEach(([key, value]) => {
|
||||||
|
expect(lastCallArgs).toContain(`${key}=${value}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on failed request', async () => {
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: false,
|
||||||
|
json: () => Promise.resolve({ error: 'Failed to fetch' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(fetchGraphDataOrThrow(defaultProps)).rejects.toThrow(
|
||||||
|
'Failed to fetch',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error on network failure', async () => {
|
||||||
|
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||||
|
|
||||||
|
await expect(fetchGraphDataOrThrow(defaultProps)).rejects.toThrow(
|
||||||
|
'Network error',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use computed start and end dates', async () => {
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockSuccessResponse),
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchGraphDataOrThrow(defaultProps);
|
||||||
|
|
||||||
|
// Verify computeStartEndDate was called with correct window length
|
||||||
|
expect(computeStartEndDate).toHaveBeenCalledWith('7D');
|
||||||
|
|
||||||
|
// Verify the computed dates are included in the URL
|
||||||
|
const lastCallArgs = mockFetch.mock.calls[0][0];
|
||||||
|
expect(lastCallArgs).toContain('start=2024-01-01');
|
||||||
|
expect(lastCallArgs).toContain('end=2024-01-07');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should construct URL with all required parameters', async () => {
|
||||||
|
mockFetch.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve(mockSuccessResponse),
|
||||||
|
});
|
||||||
|
|
||||||
|
await fetchGraphDataOrThrow(defaultProps);
|
||||||
|
|
||||||
|
const lastCallArgs = mockFetch.mock.calls[0][0];
|
||||||
|
|
||||||
|
// Check base URL
|
||||||
|
expect(lastCallArgs).toContain(
|
||||||
|
'https://api.eu-central-1.aws.tinybird.co/v0/pipes/',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check endpoint
|
||||||
|
expect(lastCallArgs).toContain('getWebhookAnalytics.json');
|
||||||
|
|
||||||
|
// Check window length options
|
||||||
|
const options = ANALYTICS_GRAPH_OPTION_MAP['7D'];
|
||||||
|
Object.entries(options).forEach(([key, value]) => {
|
||||||
|
expect(lastCallArgs).toContain(`${key}=${value}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check computed dates
|
||||||
|
expect(lastCallArgs).toContain('start=2024-01-01');
|
||||||
|
expect(lastCallArgs).toContain('end=2024-01-07');
|
||||||
|
|
||||||
|
// Check record ID
|
||||||
|
expect(lastCallArgs).toContain('webhookId=test-123');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { mapServerlessFunctionDurationToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionDurationToNivoLineInput';
|
||||||
|
|
||||||
|
describe('mapServerlessFunctionDurationToNivoLineInput', () => {
|
||||||
|
it('should convert the serverless function duration result to NivoLineInput format', () => {
|
||||||
|
const serverlessFunctionDurationResult = [
|
||||||
|
{
|
||||||
|
start: '2023-01-01T00:00:00.000Z',
|
||||||
|
minimum: 100,
|
||||||
|
maximum: 200,
|
||||||
|
average: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: '2023-01-02T00:00:00.000Z',
|
||||||
|
minimum: 80,
|
||||||
|
maximum: 160,
|
||||||
|
average: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: '2023-01-03T00:00:00.000Z',
|
||||||
|
minimum: 90,
|
||||||
|
maximum: 180,
|
||||||
|
average: 135,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: 'Maximum',
|
||||||
|
data: [
|
||||||
|
{ x: new Date('2023-01-01T00:00:00.000Z'), y: 200 },
|
||||||
|
{ x: new Date('2023-01-02T00:00:00.000Z'), y: 160 },
|
||||||
|
{ x: new Date('2023-01-03T00:00:00.000Z'), y: 180 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Minimum',
|
||||||
|
data: [
|
||||||
|
{ x: new Date('2023-01-01T00:00:00.000Z'), y: 100 },
|
||||||
|
{ x: new Date('2023-01-02T00:00:00.000Z'), y: 80 },
|
||||||
|
{ x: new Date('2023-01-03T00:00:00.000Z'), y: 90 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Average',
|
||||||
|
data: [
|
||||||
|
{ x: new Date('2023-01-01T00:00:00.000Z'), y: 150 },
|
||||||
|
{ x: new Date('2023-01-02T00:00:00.000Z'), y: 120 },
|
||||||
|
{ x: new Date('2023-01-03T00:00:00.000Z'), y: 135 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapServerlessFunctionDurationToNivoLineInput(
|
||||||
|
serverlessFunctionDurationResult,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
it('should handle an empty serverless function duration result', () => {
|
||||||
|
const serverlessFunctionDurationResult = [];
|
||||||
|
const result = mapServerlessFunctionDurationToNivoLineInput(
|
||||||
|
serverlessFunctionDurationResult,
|
||||||
|
);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { mapServerlessFunctionErrorsToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput';
|
||||||
|
|
||||||
|
describe('mapServerlessFunctionErrorsToNivoLineInput', () => {
|
||||||
|
it('should map the serverless function result to Nivo line input format for error count', () => {
|
||||||
|
const serverlessFunctionResult = [
|
||||||
|
{ start: '2023-01-01', error_count: 10, success_rate: 0.66 },
|
||||||
|
{ start: '2023-01-02', error_count: 5, success_rate: 0.75 },
|
||||||
|
{ start: '2023-01-03', error_count: 8, success_rate: 0.69 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: 'Error',
|
||||||
|
data: [
|
||||||
|
{ x: new Date('2023-01-01'), y: 10 },
|
||||||
|
{ x: new Date('2023-01-02'), y: 5 },
|
||||||
|
{ x: new Date('2023-01-03'), y: 8 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapServerlessFunctionErrorsToNivoLineInput(
|
||||||
|
serverlessFunctionResult,
|
||||||
|
'ErrorCount',
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map the serverless function result to Nivo line input format for success rate', () => {
|
||||||
|
const serverlessFunctionResult = [
|
||||||
|
{ start: '2023-01-01', error_count: 10, success_rate: 0.66 },
|
||||||
|
{ start: '2023-01-02', error_count: 5, success_rate: 0.75 },
|
||||||
|
{ start: '2023-01-03', error_count: 8, success_rate: 0.69 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: 'Success Rate',
|
||||||
|
data: [
|
||||||
|
{ x: new Date('2023-01-01'), y: 0.66 },
|
||||||
|
{ x: new Date('2023-01-02'), y: 0.75 },
|
||||||
|
{ x: new Date('2023-01-03'), y: 0.69 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapServerlessFunctionErrorsToNivoLineInput(
|
||||||
|
serverlessFunctionResult,
|
||||||
|
'SuccessRate',
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty input', () => {
|
||||||
|
const serverlessFunctionResult = [];
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: 'Error',
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const result = mapServerlessFunctionErrorsToNivoLineInput(
|
||||||
|
serverlessFunctionResult,
|
||||||
|
'ErrorCount',
|
||||||
|
);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,187 @@
|
|||||||
|
import { mapWebhookAnalyticsResultToNivoLineInput } from '@/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput';
|
||||||
|
|
||||||
|
describe('mapWebhookAnalyticsResultToNivoLineInput', () => {
|
||||||
|
it('should correctly map empty array', () => {
|
||||||
|
const result = mapWebhookAnalyticsResultToNivoLineInput([]);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly map single data point', () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
start: '2024-01-01T00:00:00Z',
|
||||||
|
success_count: 10,
|
||||||
|
failure_count: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: 'Failed',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: new Date('2024-01-01T00:00:00Z'),
|
||||||
|
y: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Succeeded',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: new Date('2024-01-01T00:00:00Z'),
|
||||||
|
y: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapWebhookAnalyticsResultToNivoLineInput(input);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly map multiple data points', () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
start: '2024-01-01T00:00:00Z',
|
||||||
|
success_count: 10,
|
||||||
|
failure_count: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: '2024-01-02T00:00:00Z',
|
||||||
|
success_count: 15,
|
||||||
|
failure_count: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: 'Failed',
|
||||||
|
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: new Date('2024-01-01T00:00:00Z'),
|
||||||
|
y: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: new Date('2024-01-02T00:00:00Z'),
|
||||||
|
y: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Succeeded',
|
||||||
|
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: new Date('2024-01-01T00:00:00Z'),
|
||||||
|
y: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: new Date('2024-01-02T00:00:00Z'),
|
||||||
|
y: 15,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapWebhookAnalyticsResultToNivoLineInput(input);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle zero counts', () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
start: '2024-01-01T00:00:00Z',
|
||||||
|
success_count: 0,
|
||||||
|
failure_count: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
id: 'Failed',
|
||||||
|
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: new Date('2024-01-01T00:00:00Z'),
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Succeeded',
|
||||||
|
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
x: new Date('2024-01-01T00:00:00Z'),
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapWebhookAnalyticsResultToNivoLineInput(input);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve data point order', () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
start: '2024-01-02T00:00:00Z',
|
||||||
|
success_count: 15,
|
||||||
|
failure_count: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: '2024-01-01T00:00:00Z',
|
||||||
|
success_count: 10,
|
||||||
|
failure_count: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapWebhookAnalyticsResultToNivoLineInput(input);
|
||||||
|
|
||||||
|
// Check that dates in data arrays maintain input order
|
||||||
|
expect(result[0].data[0].x).toEqual(new Date('2024-01-02T00:00:00Z'));
|
||||||
|
expect(result[0].data[1].x).toEqual(new Date('2024-01-01T00:00:00Z'));
|
||||||
|
expect(result[1].data[0].x).toEqual(new Date('2024-01-02T00:00:00Z'));
|
||||||
|
expect(result[1].data[1].x).toEqual(new Date('2024-01-01T00:00:00Z'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle malformed dates by creating invalid Date objects', () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
start: 'invalid-date',
|
||||||
|
success_count: 10,
|
||||||
|
failure_count: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapWebhookAnalyticsResultToNivoLineInput(input);
|
||||||
|
|
||||||
|
expect(result[0].data[0].x.toString()).toBe('Invalid Date');
|
||||||
|
expect(result[1].data[0].x.toString()).toBe('Invalid Date');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should maintain consistent structure with mixed data', () => {
|
||||||
|
const input = [
|
||||||
|
{
|
||||||
|
start: '2024-01-01T00:00:00Z',
|
||||||
|
success_count: 10,
|
||||||
|
failure_count: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: '2024-01-02T00:00:00Z',
|
||||||
|
success_count: 0,
|
||||||
|
failure_count: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = mapWebhookAnalyticsResultToNivoLineInput(input);
|
||||||
|
|
||||||
|
// Check both lines exist even when one has zero values
|
||||||
|
expect(result.length).toBe(2);
|
||||||
|
expect(result[0].data.length).toBe(2);
|
||||||
|
expect(result[1].data.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { mapServerlessFunctionDurationToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionDurationToNivoLineInput';
|
||||||
|
import { mapServerlessFunctionErrorsToNivoLineInput } from '@/analytics/utils/mapServerlessFunctionErrorsToNivoLineInput';
|
||||||
|
import { mapWebhookAnalyticsResultToNivoLineInput } from '@/analytics/utils/mapWebhookAnalyticsResultToNivoLineInput';
|
||||||
|
import { AnalyticsTinybirdJwtMap } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const computeAnalyticsGraphDataFunction = (
|
||||||
|
endpointName: keyof AnalyticsTinybirdJwtMap,
|
||||||
|
) => {
|
||||||
|
switch (endpointName) {
|
||||||
|
case 'getWebhookAnalytics':
|
||||||
|
return mapWebhookAnalyticsResultToNivoLineInput;
|
||||||
|
case 'getServerlessFunctionDuration':
|
||||||
|
return mapServerlessFunctionDurationToNivoLineInput;
|
||||||
|
case 'getServerlessFunctionErrorCount':
|
||||||
|
return (data: { start: string; error_count: number }[]) =>
|
||||||
|
mapServerlessFunctionErrorsToNivoLineInput(data, 'ErrorCount');
|
||||||
|
case 'getServerlessFunctionSuccessRate':
|
||||||
|
return (data: { start: string; success_rate: number }[]) =>
|
||||||
|
mapServerlessFunctionErrorsToNivoLineInput(data, 'SuccessRate');
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`No analytics function found associated with endpoint "${endpointName}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { subDays, subHours } from 'date-fns';
|
||||||
|
|
||||||
|
export const computeStartEndDate = (
|
||||||
|
windowLength: '7D' | '1D' | '12H' | '4H',
|
||||||
|
) => {
|
||||||
|
const now = new Date(Date.now());
|
||||||
|
const end = now.toISOString();
|
||||||
|
switch (windowLength) {
|
||||||
|
case '7D':
|
||||||
|
return {
|
||||||
|
start: subDays(now, 7).toISOString(),
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
case '1D':
|
||||||
|
return {
|
||||||
|
start: subDays(now, 1).toISOString(),
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
case '12H':
|
||||||
|
return {
|
||||||
|
start: subHours(now, 12).toISOString(),
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
case '4H':
|
||||||
|
return {
|
||||||
|
start: subHours(now, 4).toISOString(),
|
||||||
|
end,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { ANALYTICS_ENDPOINT_TYPE_MAP } from '@/analytics/constants/AnalyticsEndpointTypeMap';
|
||||||
|
import { ANALYTICS_GRAPH_OPTION_MAP } from '@/analytics/constants/AnalyticsGraphOptionMap';
|
||||||
|
import { AnalyticsComponentProps } from '@/analytics/types/AnalyticsComponentProps';
|
||||||
|
import { computeStartEndDate } from '@/analytics/utils/computeStartEndDate';
|
||||||
|
|
||||||
|
type fetchGraphDataOrThrowProps = AnalyticsComponentProps & {
|
||||||
|
windowLength: '7D' | '1D' | '12H' | '4H';
|
||||||
|
tinybirdJwt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchGraphDataOrThrow = async ({
|
||||||
|
recordId,
|
||||||
|
windowLength,
|
||||||
|
tinybirdJwt,
|
||||||
|
endpointName,
|
||||||
|
}: fetchGraphDataOrThrowProps) => {
|
||||||
|
const recordType = ANALYTICS_ENDPOINT_TYPE_MAP[endpointName];
|
||||||
|
const queryString = new URLSearchParams({
|
||||||
|
...ANALYTICS_GRAPH_OPTION_MAP[windowLength],
|
||||||
|
...computeStartEndDate(windowLength),
|
||||||
|
...{ [`${recordType}Id`]: recordId },
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.eu-central-1.aws.tinybird.co/v0/pipes/${endpointName}.json?${queryString}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + tinybirdJwt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
};
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { NivoLineInput } from '@/analytics/types/NivoLineInput';
|
||||||
|
//DOING: Adding the servelessFunctionDurationGraph to twenty
|
||||||
|
export const mapServerlessFunctionDurationToNivoLineInput = (
|
||||||
|
serverlessFunctionDurationResult: {
|
||||||
|
start: string;
|
||||||
|
minimum: number;
|
||||||
|
maximum: number;
|
||||||
|
average: number;
|
||||||
|
}[],
|
||||||
|
): NivoLineInput[] => {
|
||||||
|
return serverlessFunctionDurationResult
|
||||||
|
.flatMap((dataRow) => [
|
||||||
|
{
|
||||||
|
x: new Date(dataRow.start),
|
||||||
|
y: dataRow.maximum,
|
||||||
|
id: 'Maximum',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: new Date(dataRow.start),
|
||||||
|
y: dataRow.minimum,
|
||||||
|
id: 'Minimum',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: new Date(dataRow.start),
|
||||||
|
y: dataRow.average,
|
||||||
|
id: 'Average',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.reduce(
|
||||||
|
(
|
||||||
|
acc: NivoLineInput[],
|
||||||
|
{ id, x, y }: { id: string; x: Date; y: number },
|
||||||
|
) => {
|
||||||
|
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, data: [{ x, y }] }];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { NivoLineInput } from '@/analytics/types/NivoLineInput';
|
||||||
|
|
||||||
|
export const mapServerlessFunctionErrorsToNivoLineInput = <
|
||||||
|
T extends { start: string },
|
||||||
|
>(
|
||||||
|
serverlessFunctionResult: (T & {
|
||||||
|
error_count?: number;
|
||||||
|
success_rate?: number;
|
||||||
|
})[],
|
||||||
|
type: 'ErrorCount' | 'SuccessRate',
|
||||||
|
): NivoLineInput[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: type === 'ErrorCount' ? 'Error' : 'Success Rate',
|
||||||
|
data: serverlessFunctionResult.flatMap((dataRow) => [
|
||||||
|
{
|
||||||
|
x: new Date(dataRow.start),
|
||||||
|
y:
|
||||||
|
type === 'ErrorCount'
|
||||||
|
? (dataRow.error_count ?? 0)
|
||||||
|
: (dataRow.success_rate ?? 0),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import { NivoLineInput } from '@/analytics/types/NivoLineInput';
|
||||||
|
|
||||||
|
export const mapWebhookAnalyticsResultToNivoLineInput = (
|
||||||
|
webhookAnalyticsResult: {
|
||||||
|
start: string;
|
||||||
|
failure_count: number;
|
||||||
|
success_count: number;
|
||||||
|
}[],
|
||||||
|
): NivoLineInput[] => {
|
||||||
|
return webhookAnalyticsResult
|
||||||
|
.flatMap((dataRow) => [
|
||||||
|
{
|
||||||
|
x: new Date(dataRow.start),
|
||||||
|
y: dataRow.failure_count,
|
||||||
|
id: 'Failed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: new Date(dataRow.start),
|
||||||
|
y: dataRow.success_count,
|
||||||
|
id: 'Succeeded',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.reduce(
|
||||||
|
(
|
||||||
|
acc: NivoLineInput[],
|
||||||
|
{ id, x, y }: { id: string; x: Date; y: number },
|
||||||
|
) => {
|
||||||
|
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, data: [{ x, y }] }];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -7,7 +7,7 @@ export type CurrentUser = Pick<
|
|||||||
| 'id'
|
| 'id'
|
||||||
| 'email'
|
| 'email'
|
||||||
| 'supportUserHash'
|
| 'supportUserHash'
|
||||||
| 'analyticsTinybirdJwt'
|
| 'analyticsTinybirdJwts'
|
||||||
| 'canImpersonate'
|
| 'canImpersonate'
|
||||||
| 'onboardingStatus'
|
| 'onboardingStatus'
|
||||||
| 'userVars'
|
| 'userVars'
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
|
|
||||||
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useSetRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
type SettingsDevelopersWebhookUsageGraphEffectProps = {
|
|
||||||
webhookId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SettingsDevelopersWebhookUsageGraphEffect = ({
|
|
||||||
webhookId,
|
|
||||||
}: SettingsDevelopersWebhookUsageGraphEffectProps) => {
|
|
||||||
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
|
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
|
||||||
|
|
||||||
const { fetchGraphData } = useGraphData(webhookId);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoaded) {
|
|
||||||
fetchGraphData('7D').then((graphInput) => {
|
|
||||||
setWebhookGraphData(graphInput);
|
|
||||||
});
|
|
||||||
setIsLoaded(true);
|
|
||||||
}
|
|
||||||
}, [fetchGraphData, isLoaded, setWebhookGraphData, webhookId]);
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export const WEBHOOK_GRAPH_API_OPTIONS_MAP = {
|
|
||||||
'7D': { windowInHours: '168', tickIntervalInMinutes: '420' },
|
|
||||||
'1D': { windowInHours: '24', tickIntervalInMinutes: '60' },
|
|
||||||
'12H': { windowInHours: '12', tickIntervalInMinutes: '30' },
|
|
||||||
'4H': { windowInHours: '4', tickIntervalInMinutes: '10' },
|
|
||||||
};
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { CurrentUser, currentUserState } from '@/auth/states/currentUserState';
|
|
||||||
import { useAnalyticsTinybirdJwt } from '@/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt';
|
|
||||||
import { act } from 'react';
|
|
||||||
import { useSetRecoilState } from 'recoil';
|
|
||||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
|
||||||
|
|
||||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
|
||||||
apolloMocks: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useAnalyticsTinybirdJwt', () => {
|
|
||||||
it('should return the analytics jwt token', async () => {
|
|
||||||
const { result } = renderHook(
|
|
||||||
() => {
|
|
||||||
const setCurrentUserState = useSetRecoilState(currentUserState);
|
|
||||||
|
|
||||||
return {
|
|
||||||
useAnalyticsTinybirdJwt: useAnalyticsTinybirdJwt(),
|
|
||||||
setCurrentUserState,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{ wrapper: Wrapper },
|
|
||||||
);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setCurrentUserState({
|
|
||||||
analyticsTinybirdJwt: 'jwt',
|
|
||||||
} as CurrentUser);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.useAnalyticsTinybirdJwt).toBe('jwt');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setCurrentUserState(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.useAnalyticsTinybirdJwt).toBeUndefined();
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
result.current.setCurrentUserState({} as CurrentUser);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current.useAnalyticsTinybirdJwt).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
|
||||||
import { isNull } from '@sniptt/guards';
|
|
||||||
|
|
||||||
export const useAnalyticsTinybirdJwt = (): string | undefined => {
|
|
||||||
const currentUser = useRecoilValue(currentUserState);
|
|
||||||
|
|
||||||
if (!currentUser) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNull(currentUser.analyticsTinybirdJwt)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentUser.analyticsTinybirdJwt;
|
|
||||||
};
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import { useAnalyticsTinybirdJwt } from '@/settings/developers/webhook/hooks/useAnalyticsTinybirdJwt';
|
|
||||||
import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/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';
|
|
||||||
|
|
||||||
export const useGraphData = (webhookId: string) => {
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
|
||||||
const analyticsTinybirdJwt = useAnalyticsTinybirdJwt();
|
|
||||||
const fetchGraphData = async (
|
|
||||||
windowLengthGraphOption: '7D' | '1D' | '12H' | '4H',
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (isUndefined(analyticsTinybirdJwt)) {
|
|
||||||
throw new Error('No analyticsTinybirdJwt found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await fetchGraphDataOrThrow({
|
|
||||||
webhookId,
|
|
||||||
windowLength: windowLengthGraphOption,
|
|
||||||
tinybirdJwt: analyticsTinybirdJwt,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
enqueueSnackBar('Something went wrong while fetching webhook usage', {
|
|
||||||
variant: SnackBarVariant.Error,
|
|
||||||
});
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return { fetchGraphData };
|
|
||||||
};
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
|
|
||||||
import { createState } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const webhookGraphDataState = createState<NivoLineInput[]>({
|
|
||||||
key: 'webhookGraphData',
|
|
||||||
defaultValue: [],
|
|
||||||
});
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/constants/WebhookGraphApiOptionsMap';
|
|
||||||
import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow';
|
|
||||||
|
|
||||||
// Mock the global fetch function
|
|
||||||
global.fetch = jest.fn();
|
|
||||||
|
|
||||||
describe('fetchGraphDataOrThrow', () => {
|
|
||||||
const mockWebhookId = 'test-webhook-id';
|
|
||||||
const mockWindowLength = '7D';
|
|
||||||
const mockTinybirdJwt = 'test-jwt';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch and transform data successfully', async () => {
|
|
||||||
const mockResponse = {
|
|
||||||
ok: true,
|
|
||||||
json: jest.fn().mockResolvedValue({
|
|
||||||
data: [
|
|
||||||
{ start_interval: '2023-05-01', failure_count: 2, success_count: 8 },
|
|
||||||
{ start_interval: '2023-05-02', failure_count: 1, success_count: 9 },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
global.fetch.mockResolvedValue(mockResponse);
|
|
||||||
|
|
||||||
const result = await fetchGraphDataOrThrow({
|
|
||||||
webhookId: mockWebhookId,
|
|
||||||
windowLength: mockWindowLength,
|
|
||||||
tinybirdJwt: mockTinybirdJwt,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(global.fetch).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining(
|
|
||||||
`https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?`,
|
|
||||||
),
|
|
||||||
expect.objectContaining({
|
|
||||||
headers: {
|
|
||||||
Authorization: expect.stringContaining('Bearer '),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
id: 'Failed',
|
|
||||||
color: 'red',
|
|
||||||
data: [
|
|
||||||
{ x: '2023-05-01', y: 2 },
|
|
||||||
{ x: '2023-05-02', y: 1 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Succeeded',
|
|
||||||
color: 'blue',
|
|
||||||
data: [
|
|
||||||
{ x: '2023-05-01', y: 8 },
|
|
||||||
{ x: '2023-05-02', y: 9 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error when the response is not ok', async () => {
|
|
||||||
const mockResponse = {
|
|
||||||
ok: false,
|
|
||||||
json: jest.fn().mockResolvedValue({ error: 'Some error' }),
|
|
||||||
};
|
|
||||||
global.fetch.mockResolvedValue(mockResponse);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
fetchGraphDataOrThrow({
|
|
||||||
webhookId: mockWebhookId,
|
|
||||||
windowLength: mockWindowLength,
|
|
||||||
tinybirdJwt: mockTinybirdJwt,
|
|
||||||
}),
|
|
||||||
).rejects.toThrow('Something went wrong while fetching webhook usage');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use correct query parameters based on window length', async () => {
|
|
||||||
const mockResponse = {
|
|
||||||
ok: true,
|
|
||||||
json: jest.fn().mockResolvedValue({ data: [] }),
|
|
||||||
};
|
|
||||||
global.fetch.mockResolvedValue(mockResponse);
|
|
||||||
|
|
||||||
await fetchGraphDataOrThrow({
|
|
||||||
webhookId: mockWebhookId,
|
|
||||||
windowLength: '1D',
|
|
||||||
tinybirdJwt: mockTinybirdJwt,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(global.fetch).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining(
|
|
||||||
new URLSearchParams({
|
|
||||||
...WEBHOOK_GRAPH_API_OPTIONS_MAP['1D'],
|
|
||||||
webhookId: mockWebhookId,
|
|
||||||
}).toString(),
|
|
||||||
),
|
|
||||||
expect.any(Object),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty response data', async () => {
|
|
||||||
const mockResponse = {
|
|
||||||
ok: true,
|
|
||||||
json: jest.fn().mockResolvedValue({ data: [] }),
|
|
||||||
};
|
|
||||||
global.fetch.mockResolvedValue(mockResponse);
|
|
||||||
|
|
||||||
const result = await fetchGraphDataOrThrow({
|
|
||||||
webhookId: mockWebhookId,
|
|
||||||
windowLength: mockWindowLength,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
|
|
||||||
import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/constants/WebhookGraphApiOptionsMap';
|
|
||||||
|
|
||||||
type fetchGraphDataOrThrowProps = {
|
|
||||||
webhookId: string;
|
|
||||||
windowLength: '7D' | '1D' | '12H' | '4H';
|
|
||||||
tinybirdJwt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchGraphDataOrThrow = async ({
|
|
||||||
webhookId,
|
|
||||||
windowLength,
|
|
||||||
tinybirdJwt,
|
|
||||||
}: fetchGraphDataOrThrowProps) => {
|
|
||||||
const queryString = new URLSearchParams({
|
|
||||||
...WEBHOOK_GRAPH_API_OPTIONS_MAP[windowLength],
|
|
||||||
webhookId,
|
|
||||||
}).toString();
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?${queryString}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: 'Bearer ' + tinybirdJwt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Something went wrong while fetching webhook usage');
|
|
||||||
}
|
|
||||||
// Next steps: separate the map logic to a different component (response.data, {id:str, color:str}[])=>NivoLineInput[]
|
|
||||||
|
|
||||||
const graphInput = result.data
|
|
||||||
.flatMap(
|
|
||||||
(dataRow: {
|
|
||||||
start_interval: string;
|
|
||||||
failure_count: number;
|
|
||||||
success_count: number;
|
|
||||||
}) => [
|
|
||||||
{
|
|
||||||
x: dataRow.start_interval,
|
|
||||||
y: dataRow.failure_count,
|
|
||||||
id: 'Failed',
|
|
||||||
color: 'red', // need to refacto this
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: dataRow.start_interval,
|
|
||||||
y: dataRow.success_count,
|
|
||||||
id: 'Succeeded',
|
|
||||||
color: 'blue',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.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 }] }];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
return graphInput;
|
|
||||||
};
|
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
import { AnalyticsActivityGraph } from '@/analytics/components/AnalyticsActivityGraph';
|
||||||
|
import { AnalyticsGraphEffect } from '@/analytics/components/AnalyticsGraphEffect';
|
||||||
|
import { AnalyticsGraphDataInstanceContext } from '@/analytics/states/contexts/AnalyticsGraphDataInstanceContext';
|
||||||
|
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionMonitoringTab = ({
|
||||||
|
serverlessFunctionId,
|
||||||
|
}: {
|
||||||
|
serverlessFunctionId: string;
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useHotkeyScopeOnMount(
|
||||||
|
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionSettingsTab,
|
||||||
|
);
|
||||||
|
|
||||||
|
useScopedHotkeys(
|
||||||
|
[Key.Escape],
|
||||||
|
() => {
|
||||||
|
navigate(getSettingsPagePath(SettingsPath.ServerlessFunctions));
|
||||||
|
},
|
||||||
|
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionSettingsTab,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AnalyticsGraphDataInstanceContext.Provider
|
||||||
|
value={{
|
||||||
|
instanceId: `function-${serverlessFunctionId}-errorCount`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnalyticsGraphEffect
|
||||||
|
recordId={serverlessFunctionId}
|
||||||
|
endpointName="getServerlessFunctionErrorCount"
|
||||||
|
/>
|
||||||
|
<AnalyticsActivityGraph
|
||||||
|
recordId={serverlessFunctionId}
|
||||||
|
endpointName="getServerlessFunctionErrorCount"
|
||||||
|
/>
|
||||||
|
</AnalyticsGraphDataInstanceContext.Provider>
|
||||||
|
|
||||||
|
<AnalyticsGraphDataInstanceContext.Provider
|
||||||
|
value={{ instanceId: `function-${serverlessFunctionId}-duration` }}
|
||||||
|
>
|
||||||
|
<AnalyticsGraphEffect
|
||||||
|
recordId={serverlessFunctionId}
|
||||||
|
endpointName="getServerlessFunctionDuration"
|
||||||
|
/>
|
||||||
|
<AnalyticsActivityGraph
|
||||||
|
recordId={serverlessFunctionId}
|
||||||
|
endpointName="getServerlessFunctionDuration"
|
||||||
|
/>
|
||||||
|
</AnalyticsGraphDataInstanceContext.Provider>
|
||||||
|
|
||||||
|
<AnalyticsGraphDataInstanceContext.Provider
|
||||||
|
value={{ instanceId: `function-${serverlessFunctionId}-successRate` }}
|
||||||
|
>
|
||||||
|
<AnalyticsGraphEffect
|
||||||
|
recordId={serverlessFunctionId}
|
||||||
|
endpointName="getServerlessFunctionSuccessRate"
|
||||||
|
/>
|
||||||
|
<AnalyticsActivityGraph
|
||||||
|
recordId={serverlessFunctionId}
|
||||||
|
endpointName="getServerlessFunctionSuccessRate"
|
||||||
|
/>
|
||||||
|
</AnalyticsGraphDataInstanceContext.Provider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import TextareaAutosize from 'react-textarea-autosize';
|
|||||||
|
|
||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
|
|
||||||
|
import { RGBA } from 'twenty-ui';
|
||||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||||
|
|
||||||
@ -51,6 +52,10 @@ const StyledTextArea = styled(TextareaAutosize)`
|
|||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
${({ theme }) => {
|
||||||
|
return `box-shadow: 0px 0px 0px 3px ${RGBA(theme.color.blue, 0.1)};
|
||||||
|
border-color: ${theme.color.blue};`;
|
||||||
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
|
|||||||
@ -8,7 +8,14 @@ export const USER_QUERY_FRAGMENT = gql`
|
|||||||
email
|
email
|
||||||
canImpersonate
|
canImpersonate
|
||||||
supportUserHash
|
supportUserHash
|
||||||
analyticsTinybirdJwt
|
analyticsTinybirdJwts {
|
||||||
|
getWebhookAnalytics
|
||||||
|
getPageviewsAnalytics
|
||||||
|
getUsersAnalytics
|
||||||
|
getServerlessFunctionDuration
|
||||||
|
getServerlessFunctionSuccessRate
|
||||||
|
getServerlessFunctionErrorCount
|
||||||
|
}
|
||||||
onboardingStatus
|
onboardingStatus
|
||||||
workspaceMember {
|
workspaceMember {
|
||||||
...WorkspaceMemberQueryFragment
|
...WorkspaceMemberQueryFragment
|
||||||
|
|||||||
@ -15,6 +15,9 @@ import {
|
|||||||
useIcons,
|
useIcons,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
import { AnalyticsActivityGraph } from '@/analytics/components/AnalyticsActivityGraph';
|
||||||
|
import { AnalyticsGraphEffect } from '@/analytics/components/AnalyticsGraphEffect';
|
||||||
|
import { AnalyticsGraphDataInstanceContext } from '@/analytics/states/contexts/AnalyticsGraphDataInstanceContext';
|
||||||
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
||||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
@ -24,8 +27,6 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
|||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
||||||
import { SettingsDevelopersWebhookUsageGraph } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
|
|
||||||
import { SettingsDevelopersWebhookUsageGraphEffect } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect';
|
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
@ -281,10 +282,18 @@ export const SettingsDevelopersWebhooksDetail = () => {
|
|||||||
))}
|
))}
|
||||||
</Section>
|
</Section>
|
||||||
{isAnalyticsEnabled && isAnalyticsV2Enabled && (
|
{isAnalyticsEnabled && isAnalyticsV2Enabled && (
|
||||||
<>
|
<AnalyticsGraphDataInstanceContext.Provider
|
||||||
<SettingsDevelopersWebhookUsageGraphEffect webhookId={webhookId} />
|
value={{ instanceId: `webhook-${webhookId}-analytics` }}
|
||||||
<SettingsDevelopersWebhookUsageGraph webhookId={webhookId} />
|
>
|
||||||
</>
|
<AnalyticsGraphEffect
|
||||||
|
recordId={webhookId}
|
||||||
|
endpointName="getWebhookAnalytics"
|
||||||
|
/>
|
||||||
|
<AnalyticsActivityGraph
|
||||||
|
recordId={webhookId}
|
||||||
|
endpointName="getWebhookAnalytics"
|
||||||
|
/>
|
||||||
|
</AnalyticsGraphDataInstanceContext.Provider>
|
||||||
)}
|
)}
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title title="Danger zone" description="Delete this integration" />
|
<H2Title title="Danger zone" description="Delete this integration" />
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsServerlessFunctionCodeEditorTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab';
|
import { SettingsServerlessFunctionCodeEditorTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab';
|
||||||
|
import { SettingsServerlessFunctionMonitoringTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionMonitoringTab';
|
||||||
import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab';
|
import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab';
|
||||||
import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab';
|
import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab';
|
||||||
import { SettingsServerlessFunctionTestTabEffect } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTabEffect';
|
import { SettingsServerlessFunctionTestTabEffect } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTabEffect';
|
||||||
@ -17,10 +19,17 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { IconCode, IconSettings, IconTestPipe, Section } from 'twenty-ui';
|
import {
|
||||||
|
IconCode,
|
||||||
|
IconGauge,
|
||||||
|
IconSettings,
|
||||||
|
IconTestPipe,
|
||||||
|
Section,
|
||||||
|
} from 'twenty-ui';
|
||||||
import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback';
|
import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -169,11 +178,17 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
}
|
}
|
||||||
setActiveTabId('test');
|
setActiveTabId('test');
|
||||||
};
|
};
|
||||||
|
const isAnalyticsEnabled = useRecoilValue(isAnalyticsEnabledState);
|
||||||
|
|
||||||
|
const isAnalyticsV2Enabled = useIsFeatureEnabled('IS_ANALYTICS_V2_ENABLED');
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'editor', title: 'Editor', Icon: IconCode },
|
{ id: 'editor', title: 'Editor', Icon: IconCode },
|
||||||
{ id: 'test', title: 'Test', Icon: IconTestPipe },
|
{ id: 'test', title: 'Test', Icon: IconTestPipe },
|
||||||
{ id: 'settings', title: 'Settings', Icon: IconSettings },
|
{ id: 'settings', title: 'Settings', Icon: IconSettings },
|
||||||
|
...(isAnalyticsEnabled && isAnalyticsV2Enabled
|
||||||
|
? [{ id: 'monitoring', title: 'Monitoring', Icon: IconGauge }]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const files = formValues.code
|
const files = formValues.code
|
||||||
@ -219,6 +234,12 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
onCodeChange={onCodeChange}
|
onCodeChange={onCodeChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'monitoring':
|
||||||
|
return (
|
||||||
|
<SettingsServerlessFunctionMonitoringTab
|
||||||
|
serverlessFunctionId={serverlessFunctionId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ type MockedUser = Pick<
|
|||||||
| 'supportUserHash'
|
| 'supportUserHash'
|
||||||
| 'onboardingStatus'
|
| 'onboardingStatus'
|
||||||
| 'userVars'
|
| 'userVars'
|
||||||
|
| 'analyticsTinybirdJwts'
|
||||||
> & {
|
> & {
|
||||||
workspaceMember: WorkspaceMember | null;
|
workspaceMember: WorkspaceMember | null;
|
||||||
locale: string;
|
locale: string;
|
||||||
@ -113,6 +114,7 @@ export const mockedUserData: MockedUser = {
|
|||||||
workspaceMembers: [mockedWorkspaceMemberData],
|
workspaceMembers: [mockedWorkspaceMemberData],
|
||||||
onboardingStatus: OnboardingStatus.Completed,
|
onboardingStatus: OnboardingStatus.Completed,
|
||||||
userVars: {},
|
userVars: {},
|
||||||
|
analyticsTinybirdJwts: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockedOnboardingUserData = (
|
export const mockedOnboardingUserData = (
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
|
|
||||||
import { Analytics } from './analytics.entity';
|
|
||||||
import { AnalyticsService } from './analytics.service';
|
import { AnalyticsService } from './analytics.service';
|
||||||
|
|
||||||
import { CreateAnalyticsInput } from './dtos/create-analytics.input';
|
import { CreateAnalyticsInput } from './dtos/create-analytics.input';
|
||||||
|
import { Analytics } from './entities/analytics.entity';
|
||||||
|
|
||||||
@Resolver(() => Analytics)
|
@Resolver(() => Analytics)
|
||||||
export class AnalyticsResolver {
|
export class AnalyticsResolver {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
|
|
||||||
import { AxiosRequestConfig } from 'axios';
|
import { AxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
import { AnalyticsTinybirdJwtMap } from 'src/engine/core-modules/analytics/entities/analytics-tinybird-jwts.entity';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||||
|
|
||||||
@ -89,24 +90,56 @@ export class AnalyticsService {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateWorkspaceJwt(workspaceId: string | undefined) {
|
generateWorkspaceJwt(
|
||||||
const pipeId = 't_b49e0fe60f9e438eae81cb31c5260df2'; // refactor this pass as params
|
workspaceId: string | undefined,
|
||||||
//perhaps a constant of name:pipeId??? better typing in this func^
|
): AnalyticsTinybirdJwtMap | null {
|
||||||
const payload = {
|
if (!this.environmentService.get('ANALYTICS_ENABLED')) {
|
||||||
name: 'my_demo_jwt',
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jwtPayload = {
|
||||||
|
name: 'analytics_jwt',
|
||||||
workspace_id: this.environmentService.get('TINYBIRD_WORKSPACE_UUID'),
|
workspace_id: this.environmentService.get('TINYBIRD_WORKSPACE_UUID'),
|
||||||
scopes: [
|
scopes: [
|
||||||
{
|
{
|
||||||
type: 'PIPES:READ',
|
type: 'PIPES:READ',
|
||||||
resource: pipeId,
|
resource: '',
|
||||||
fixed_params: { workspaceId: workspaceId },
|
fixed_params: { workspaceId },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.jwtWrapperService.sign(payload, {
|
const jwtOptions = {
|
||||||
secret: this.environmentService.get('TINYBIRD_GENERATE_JWT_TOKEN'),
|
secret: this.environmentService.get('TINYBIRD_GENERATE_JWT_TOKEN'),
|
||||||
expiresIn: '7d',
|
expiresIn: '7d',
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const analyticsProperties = [
|
||||||
|
'getWebhookAnalytics',
|
||||||
|
'getPageviewsAnalytics',
|
||||||
|
'getUsersAnalytics',
|
||||||
|
'getServerlessFunctionDuration',
|
||||||
|
'getServerlessFunctionSuccessRate',
|
||||||
|
'getServerlessFunctionErrorCount',
|
||||||
|
];
|
||||||
|
|
||||||
|
return analyticsProperties.reduce(
|
||||||
|
(acc, property) => ({
|
||||||
|
...acc,
|
||||||
|
[property]: this.jwtWrapperService.sign(
|
||||||
|
{
|
||||||
|
...jwtPayload,
|
||||||
|
scopes: [
|
||||||
|
{
|
||||||
|
...jwtPayload.scopes[0],
|
||||||
|
resource: property,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
jwtOptions,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
) as AnalyticsTinybirdJwtMap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class AnalyticsTinybirdJwtMap {
|
||||||
|
@Field(() => String)
|
||||||
|
getWebhookAnalytics: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
getPageviewsAnalytics: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
getUsersAnalytics: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
getServerlessFunctionDuration: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
getServerlessFunctionSuccessRate: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
getServerlessFunctionErrorCount: string;
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/su
|
|||||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||||
|
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
|
import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service';
|
||||||
|
import { AnalyticsTinybirdJwtMap } from 'src/engine/core-modules/analytics/entities/analytics-tinybird-jwts.entity';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||||
@ -156,13 +157,9 @@ export class UserResolver {
|
|||||||
return getHMACKey(parent.email, key);
|
return getHMACKey(parent.email, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => String, {
|
@ResolveField(() => AnalyticsTinybirdJwtMap, { nullable: true })
|
||||||
nullable: true,
|
analyticsTinybirdJwts(@AuthWorkspace() workspace: Workspace | undefined) {
|
||||||
})
|
return this.analyticsService.generateWorkspaceJwt(workspace?.id);
|
||||||
async analyticsTinybirdJwt(
|
|
||||||
@AuthWorkspace() workspace: Workspace | undefined,
|
|
||||||
): Promise<string> {
|
|
||||||
return await this.analyticsService.generateWorkspaceJwt(workspace?.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => String)
|
@Mutation(() => String)
|
||||||
|
|||||||
@ -23,6 +23,12 @@ Copy the "admin your@email" token from from https://app.tinybird.co/tokens and p
|
|||||||
** Auth successful!
|
** Auth successful!
|
||||||
** Configuration written to .tinyb file, consider adding it to .gitignore
|
** Configuration written to .tinyb file, consider adding it to .gitignore
|
||||||
```
|
```
|
||||||
|
You can also log in using your twenty_analytics_token without passing into the interactive mode:
|
||||||
|
```sh
|
||||||
|
tb auth --token <your twenty_analytics_token >
|
||||||
|
** Auth successful!
|
||||||
|
** Configuration written to .tinyb file, consider adding it to .gitignore
|
||||||
|
```
|
||||||
To sync your changes to Tinybird use:
|
To sync your changes to Tinybird use:
|
||||||
```sh
|
```sh
|
||||||
tb push --force --push-deps
|
tb push --force --push-deps
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
SCHEMA >
|
SCHEMA >
|
||||||
`action` String `json:$.action`,
|
`action` LowCardinality(String) `json:$.action`,
|
||||||
`timestamp` DateTime64(3) `json:$.timestamp`,
|
`timestamp` DateTime64(3) `json:$.timestamp`,
|
||||||
`version` String `json:$.version`,
|
`version` LowCardinality(String) `json:$.version`,
|
||||||
`userId` String `json:$.userId` DEFAULT '',
|
`userId` String `json:$.userId` DEFAULT '',
|
||||||
`workspaceId` String `json:$.workspaceId` DEFAULT '',
|
`workspaceId` String `json:$.workspaceId` DEFAULT '',
|
||||||
`payload` String `json:$.payload`
|
`payload` String `json:$.payload`
|
||||||
|
|
||||||
ENGINE MergeTree
|
ENGINE MergeTree
|
||||||
ENGINE_PARTITION_KEY toYear(timestamp)
|
ENGINE_PARTITION_KEY toYear(timestamp)
|
||||||
ENGINE_SORTING_KEY action, timestamp
|
ENGINE_SORTING_KEY action, workspaceId, timestamp
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
SCHEMA >
|
SCHEMA >
|
||||||
`href` String `json:$.href`,
|
`href` String `json:$.href`,
|
||||||
`locale` String `json:$.locale`,
|
`locale` LowCardinality(String) `json:$.locale`,
|
||||||
`pathname` String `json:$.pathname`,
|
`pathname` String `json:$.pathname`,
|
||||||
`referrer` String `json:$.referrer`,
|
`referrer` String `json:$.referrer`,
|
||||||
`sessionId` String `json:$.sessionId`,
|
`sessionId` String `json:$.sessionId`,
|
||||||
`timeZone` String `json:$.timeZone`,
|
`timeZone` LowCardinality(String) `json:$.timeZone`,
|
||||||
`timestamp` DateTime64(3) `json:$.timestamp`,
|
`timestamp` DateTime64(3) `json:$.timestamp`,
|
||||||
`userAgent` String `json:$.userAgent`,
|
`userAgent` String `json:$.userAgent`,
|
||||||
`userId` String `json:$.userId` DEFAULT '',
|
`userId` String `json:$.userId` DEFAULT '',
|
||||||
`version` String `json:$.version`,
|
`version` LowCardinality(String) `json:$.version`,
|
||||||
`workspaceId` String `json:$.workspaceId`
|
`workspaceId` String `json:$.workspaceId` DEFAULT ''
|
||||||
|
|
||||||
ENGINE MergeTree
|
ENGINE MergeTree
|
||||||
ENGINE_PARTITION_KEY toYear(timestamp)
|
ENGINE_PARTITION_KEY toYear(timestamp)
|
||||||
ENGINE_SORTING_KEY timestamp, userId, version, workspaceId
|
ENGINE_SORTING_KEY workspaceId, userId, timestamp
|
||||||
|
|||||||
@ -4,8 +4,9 @@ SCHEMA >
|
|||||||
`functionId` String,
|
`functionId` String,
|
||||||
`durationInMs` Int64,
|
`durationInMs` Int64,
|
||||||
`success` Bool,
|
`success` Bool,
|
||||||
`errorType` String
|
`errorType` LowCardinality(String),
|
||||||
|
`version` LowCardinality(String)
|
||||||
|
|
||||||
ENGINE MergeTree
|
ENGINE MergeTree
|
||||||
ENGINE_PARTITION_KEY toYYYYMM(timestamp)
|
ENGINE_PARTITION_KEY toYYYYMM(timestamp)
|
||||||
ENGINE_SORTING_KEY timestamp, functionId, success
|
ENGINE_SORTING_KEY workspaceId, functionId, timestamp
|
||||||
|
|||||||
@ -3,11 +3,11 @@ SCHEMA >
|
|||||||
`workspaceId` String,
|
`workspaceId` String,
|
||||||
`webhookId` String,
|
`webhookId` String,
|
||||||
`url` String,
|
`url` String,
|
||||||
`success` UInt8,
|
`success` Bool,
|
||||||
`status` Int64,
|
`status` Int64,
|
||||||
`eventName` String,
|
`eventName` LowCardinality(String),
|
||||||
`version` String
|
`version` LowCardinality(String)
|
||||||
|
|
||||||
ENGINE MergeTree
|
ENGINE MergeTree
|
||||||
ENGINE_PARTITION_KEY toYYYYMM(timestamp)
|
ENGINE_PARTITION_KEY toYYYYMM(timestamp)
|
||||||
ENGINE_SORTING_KEY timestamp, workspaceId
|
ENGINE_SORTING_KEY workspaceId, webhookId, timestamp
|
||||||
|
|||||||
@ -6,18 +6,14 @@ SQL >
|
|||||||
%
|
%
|
||||||
WITH
|
WITH
|
||||||
toStartOfDay(
|
toStartOfDay(
|
||||||
toDateTime64({{ DateTime64(start, '2024-10-16 00:00:00.000') }}, 3),
|
parseDateTime64BestEffort({{ String(start, '2024-11-01T00:00:00.000Z') }}, 3)
|
||||||
{{ String(timezone, 'UTC') }}
|
|
||||||
) AS start,
|
) AS start,
|
||||||
toStartOfDay(
|
toStartOfDay(parseDateTime64BestEffort({{ String(end, '2024-11-02T00:00:00.000Z') }}, 3)) AS
|
||||||
toDateTime64({{ DateTime64(end, '2024-10-23 00:00:00.000') }}, 3),
|
|
||||||
{{ String(timezone, 'UTC') }}
|
|
||||||
) AS
|
|
||||||
end
|
end
|
||||||
SELECT
|
SELECT
|
||||||
arrayJoin(
|
arrayJoin(
|
||||||
arrayMap(
|
arrayMap(
|
||||||
x -> toDateTime64(toStartOfDay(toDateTime64(x, 3), {{ String(timezone, 'UTC') }}), 3),
|
x -> toDateTime64(toStartOfDay(toDateTime64(x, 3)), 3),
|
||||||
range(toUInt32(start + 86400), toUInt32(end + 86400),
|
range(toUInt32(start + 86400), toUInt32(end + 86400),
|
||||||
86400
|
86400
|
||||||
)
|
)
|
||||||
@ -29,13 +25,9 @@ SQL >
|
|||||||
%
|
%
|
||||||
WITH
|
WITH
|
||||||
toStartOfHour(
|
toStartOfHour(
|
||||||
toDateTime64({{ DateTime64(start, '2024-10-22 00:00:00.000') }}, 3),
|
parseDateTime64BestEffort({{ String(start, '2024-11-01T00:00:00.000Z') }}, 3)
|
||||||
{{ String(timezone, 'UTC') }}
|
|
||||||
) AS start,
|
) AS start,
|
||||||
toStartOfHour(
|
toStartOfHour(parseDateTime64BestEffort({{ String(end, '2024-11-02T00:00:00.000Z') }}, 3)) AS
|
||||||
toDateTime64({{ DateTime64(end, '2024-10-23 00:00:00.000') }}, 3),
|
|
||||||
{{ String(timezone, 'UTC') }}
|
|
||||||
) AS
|
|
||||||
end
|
end
|
||||||
SELECT
|
SELECT
|
||||||
arrayJoin(
|
arrayJoin(
|
||||||
@ -43,45 +35,7 @@ SQL >
|
|||||||
)
|
)
|
||||||
) as interval
|
) as interval
|
||||||
|
|
||||||
NODE customIntervals
|
|
||||||
SQL >
|
|
||||||
%
|
|
||||||
WITH
|
|
||||||
time_series AS (
|
|
||||||
SELECT
|
|
||||||
toDateTime64(
|
|
||||||
toDateTime(
|
|
||||||
toStartOfInterval(
|
|
||||||
toDateTime64({{ DateTime64(start, '2024-10-22 00:00:00.000') }}, 3),
|
|
||||||
INTERVAL {{ Int32(tickIntervalInMinutes, 420) }} MINUTE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
+ INTERVAL number * {{ Int32(tickIntervalInMinutes, 420) }} MINUTE,
|
|
||||||
3
|
|
||||||
) AS interval
|
|
||||||
FROM
|
|
||||||
numbers(
|
|
||||||
0,
|
|
||||||
1 + intDiv(
|
|
||||||
dateDiff(
|
|
||||||
'minute',
|
|
||||||
toDateTime64({{ DateTime64(start, '2024-10-22 00:00:00.000') }}, 3),
|
|
||||||
toDateTime64({{ DateTime64(end, '2024-10-23 00:00:00.000') }}, 3)
|
|
||||||
),
|
|
||||||
{{ Int32(tickIntervalInMinutes, 420) }}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
WHERE interval <= toDateTime64({{ DateTime64(end, '2024-10-23 00:00:00.000') }}, 3)
|
|
||||||
)
|
|
||||||
SELECT interval
|
|
||||||
FROM time_series
|
|
||||||
|
|
||||||
NODE selectIntervalByGranularity
|
NODE selectIntervalByGranularity
|
||||||
SQL >
|
SQL >
|
||||||
%
|
%
|
||||||
SELECT *
|
SELECT * FROM {% if granularity == "hour" %} hourIntervals {% else %} dayIntervals {% end %}
|
||||||
FROM
|
|
||||||
{% if granularity == "custom" %} customIntervals
|
|
||||||
{% elif granularity == "hour" %} hourIntervals
|
|
||||||
{% else %} dayIntervals
|
|
||||||
{% end %}
|
|
||||||
|
|||||||
@ -4,14 +4,8 @@ NODE timeSeriesServerlessFunctionDurationData
|
|||||||
SQL >
|
SQL >
|
||||||
%
|
%
|
||||||
SELECT
|
SELECT
|
||||||
{% if granularity == "hour" %} toStartOfHour(timestamp, {{ String(timezone, 'UTC') }})
|
{% if granularity == "hour" %} toStartOfHour(timestamp)
|
||||||
{% elif granularity == "custom" %}
|
{% else %} toDateTime64(toStartOfDay(timestamp), 3)
|
||||||
toDateTime64(
|
|
||||||
toStartOfMinute(timestamp, {{ String(timezone, 'UTC') }}),
|
|
||||||
3,
|
|
||||||
{{ String(timezone, 'UTC') }}
|
|
||||||
)
|
|
||||||
{% else %} toDateTime64(toStartOfDay(timestamp, {{ String(timezone, 'UTC') }}), 3)
|
|
||||||
{% end %} AS interval,
|
{% end %} AS interval,
|
||||||
avg(CAST(durationInMs AS Float64)) as average,
|
avg(CAST(durationInMs AS Float64)) as average,
|
||||||
min(durationInMs) as minimum,
|
min(durationInMs) as minimum,
|
||||||
@ -19,11 +13,11 @@ SQL >
|
|||||||
FROM serverlessFunctionEventMV
|
FROM serverlessFunctionEventMV
|
||||||
WHERE
|
WHERE
|
||||||
true
|
true
|
||||||
AND functionId = {{ String(functionId, 'a9fd87c0-af86-4e17-be3a-a6d3d961678a', required=True) }}
|
|
||||||
AND workspaceId
|
AND workspaceId
|
||||||
={{ String(workspaceId, '20202020-1c25-4d02-bf25-6aeccf7ea419', required=True) }}
|
={{ String(workspaceId, '20202020-1c25-4d02-bf25-6aeccf7ea419', required=True) }}
|
||||||
AND timestamp >= {{ DateTime(start, '2024-10-22 00:00:00') }}
|
AND functionId = {{ String(functionId, 'a9fd87c0-af86-4e17-be3a-a6d3d961678a', required=True) }}
|
||||||
AND timestamp < {{ DateTime(end, '2024-10-23 00:00:00') }}
|
AND timestamp >= parseDateTime64BestEffort({{ String(start, '2024-11-01T00:00:00.000Z') }}, 3)
|
||||||
|
AND timestamp < parseDateTime64BestEffort({{ String(end, '2024-11-02T00:00:00.000Z') }}, 3)
|
||||||
GROUP BY interval
|
GROUP BY interval
|
||||||
ORDER BY interval
|
ORDER BY interval
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
INCLUDE "../includes/timeSeries.incl"
|
||||||
|
-- I decided to separate the error count and the success rate because I think we should maintain the bijection
|
||||||
|
-- between an endpoint and a graph in the front-end.
|
||||||
|
NODE timeSeriesServerlessFunctionErrorCountData
|
||||||
|
SQL >
|
||||||
|
%
|
||||||
|
SELECT
|
||||||
|
{% if granularity == "hour" %} toStartOfHour(timestamp)
|
||||||
|
{% else %} toDateTime64(toStartOfDay(timestamp), 3)
|
||||||
|
{% end %} AS interval,
|
||||||
|
uniqIf(*, success = false) as error_count
|
||||||
|
FROM serverlessFunctionEventMV
|
||||||
|
WHERE
|
||||||
|
true
|
||||||
|
AND workspaceId
|
||||||
|
={{ String(workspaceId, '20202020-1c25-4d02-bf25-6aeccf7ea419', required=True) }}
|
||||||
|
AND functionId = {{ String(functionId, 'ad018fc5-eace-4f7e-942f-929560a16459', required=True) }}
|
||||||
|
AND timestamp >= parseDateTime64BestEffort({{ String(start, '2024-11-01T00:00:00.000Z') }}, 3)
|
||||||
|
AND timestamp < parseDateTime64BestEffort({{ String(end, '2024-11-02T00:00:00.000Z') }}, 3)
|
||||||
|
GROUP BY interval
|
||||||
|
ORDER BY interval
|
||||||
|
|
||||||
|
NODE endpoint
|
||||||
|
SQL >
|
||||||
|
%
|
||||||
|
SELECT formatDateTime(interval, '%FT%T.000%z') as start, error_count
|
||||||
|
FROM selectIntervalByGranularity
|
||||||
|
LEFT JOIN timeSeriesServerlessFunctionErrorCountData USING interval
|
||||||
@ -1,41 +0,0 @@
|
|||||||
INCLUDE "../includes/timeSeries.incl"
|
|
||||||
|
|
||||||
NODE timeSeriesServerlessFunctionErrorsData
|
|
||||||
SQL >
|
|
||||||
%
|
|
||||||
SELECT
|
|
||||||
{% if granularity == "hour" %} toStartOfHour(timestamp, {{ String(timezone, 'UTC') }})
|
|
||||||
{% elif granularity == "custom" %}
|
|
||||||
toDateTime64(
|
|
||||||
toStartOfMinute(timestamp, {{ String(timezone, 'UTC') }}),
|
|
||||||
3,
|
|
||||||
{{ String(timezone, 'UTC') }}
|
|
||||||
)
|
|
||||||
{% else %} toDateTime64(toStartOfDay(timestamp, {{ String(timezone, 'UTC') }}), 3)
|
|
||||||
{% end %} AS interval,
|
|
||||||
uniqIf(*, success = false) as error_count,
|
|
||||||
round(
|
|
||||||
if(
|
|
||||||
uniqIf(*, success = true) = 0,
|
|
||||||
0,
|
|
||||||
(uniqIf(*, success = true) - uniqIf(*, success = false)) / uniqIf(*, success = true)
|
|
||||||
),
|
|
||||||
2
|
|
||||||
) as success_rate
|
|
||||||
FROM serverlessFunctionEventMV
|
|
||||||
WHERE
|
|
||||||
true
|
|
||||||
AND functionId = {{ String(functionId, 'a9fd87c0-af86-4e17-be3a-a6d3d961678a', required=True) }}
|
|
||||||
AND workspaceId
|
|
||||||
={{ String(workspaceId, '20202020-1c25-4d02-bf25-6aeccf7ea419', required=True) }}
|
|
||||||
AND timestamp >= {{ DateTime(start, '2024-10-22 00:00:00') }}
|
|
||||||
AND timestamp < {{ DateTime(end, '2024-10-23 00:00:00') }}
|
|
||||||
GROUP BY interval
|
|
||||||
ORDER BY interval
|
|
||||||
|
|
||||||
NODE endpoint
|
|
||||||
SQL >
|
|
||||||
%
|
|
||||||
SELECT formatDateTime(interval, '%FT%T.000%z') as start, error_count, success_rate
|
|
||||||
FROM selectIntervalByGranularity
|
|
||||||
LEFT JOIN timeSeriesServerlessFunctionErrorsData USING interval
|
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
INCLUDE "../includes/timeSeries.incl"
|
||||||
|
|
||||||
|
NODE timeSeriesServerlessFunctionSuccessRateData
|
||||||
|
SQL >
|
||||||
|
%
|
||||||
|
SELECT
|
||||||
|
{% if granularity == "hour" %} toStartOfHour(timestamp)
|
||||||
|
{% else %} toDateTime64(toStartOfDay(timestamp), 3)
|
||||||
|
{% end %} AS interval,
|
||||||
|
round(
|
||||||
|
if(
|
||||||
|
uniqIf(*, success = true) = 0,
|
||||||
|
0,
|
||||||
|
(uniqIf(*, success = true) - uniqIf(*, success = false)) / uniqIf(*, success = true)
|
||||||
|
),
|
||||||
|
2
|
||||||
|
) as success_rate
|
||||||
|
FROM serverlessFunctionEventMV
|
||||||
|
WHERE
|
||||||
|
true
|
||||||
|
AND workspaceId
|
||||||
|
={{ String(workspaceId, '20202020-1c25-4d02-bf25-6aeccf7ea419', required=True) }}
|
||||||
|
AND functionId = {{ String(functionId, 'ad018fc5-eace-4f7e-942f-929560a16459', required=True) }}
|
||||||
|
AND timestamp >= parseDateTime64BestEffort({{ String(start, '2024-11-01T00:00:00.000Z') }}, 3)
|
||||||
|
AND timestamp < parseDateTime64BestEffort({{ String(end, '2024-11-02T00:00:00.000Z') }}, 3)
|
||||||
|
GROUP BY interval
|
||||||
|
ORDER BY interval
|
||||||
|
|
||||||
|
NODE endpoint
|
||||||
|
SQL >
|
||||||
|
%
|
||||||
|
SELECT formatDateTime(interval, '%FT%T.000%z') as start, success_rate
|
||||||
|
FROM selectIntervalByGranularity
|
||||||
|
LEFT JOIN timeSeriesServerlessFunctionSuccessRateData USING interval
|
||||||
@ -4,25 +4,19 @@ NODE timeSeriesWebhookData
|
|||||||
SQL >
|
SQL >
|
||||||
%
|
%
|
||||||
SELECT
|
SELECT
|
||||||
{% if granularity == "hour" %} toStartOfHour(timestamp, {{ String(timezone, 'UTC') }})
|
{% if granularity == "hour" %} toStartOfHour(timestamp)
|
||||||
{% elif granularity == "custom" %}
|
{% else %} toDateTime64(toStartOfDay(timestamp), 3)
|
||||||
toDateTime64(
|
|
||||||
toStartOfMinute(timestamp, {{ String(timezone, 'UTC') }}),
|
|
||||||
3,
|
|
||||||
{{ String(timezone, 'UTC') }}
|
|
||||||
)
|
|
||||||
{% else %} toDateTime64(toStartOfDay(timestamp, {{ String(timezone, 'UTC') }}), 3)
|
|
||||||
{% end %} AS interval,
|
{% end %} AS interval,
|
||||||
uniqIf(*, success = true) as success_count,
|
uniqIf(*, success = true) as success_count,
|
||||||
uniqIf(*, success = false) as failure_count
|
uniqIf(*, success = false) as failure_count
|
||||||
FROM webhookEventMV
|
FROM webhookEventMV
|
||||||
WHERE
|
WHERE
|
||||||
true
|
true
|
||||||
AND webhookId = {{ String(webhookId, '90f12aed-0276-4bea-bcaa-c21ea2763d7d', required=True) }}
|
|
||||||
AND workspaceId
|
AND workspaceId
|
||||||
={{ String(workspaceId, '20202020-1c25-4d02-bf25-6aeccf7ea419', required=True) }}
|
={{ String(workspaceId, '20202020-1c25-4d02-bf25-6aeccf7ea419', required=True) }}
|
||||||
AND timestamp >= {{ DateTime(start, '2024-10-22 00:00:00') }}
|
AND webhookId = {{ String(webhookId, '5237a3bc-566d-4290-b951-96e91051f968', required=True) }}
|
||||||
AND timestamp < {{ DateTime(end, '2024-10-23 00:00:00') }}
|
AND timestamp >= parseDateTime64BestEffort({{ String(start, '2024-11-01T00:00:00.000Z') }}, 3)
|
||||||
|
AND timestamp < parseDateTime64BestEffort({{ String(end, '2024-11-02T00:00:00.000Z') }}, 3)
|
||||||
GROUP BY interval
|
GROUP BY interval
|
||||||
ORDER BY interval
|
ORDER BY interval
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,8 @@ SQL >
|
|||||||
JSONExtractString(payload, 'functionId') as functionId,
|
JSONExtractString(payload, 'functionId') as functionId,
|
||||||
JSONExtractInt(payload, 'duration') as durationInMs,
|
JSONExtractInt(payload, 'duration') as durationInMs,
|
||||||
if(JSONExtractString(payload, 'status') = 'SUCCESS', TRUE, FALSE) as success,
|
if(JSONExtractString(payload, 'status') = 'SUCCESS', TRUE, FALSE) as success,
|
||||||
JSONExtractString(payload, 'errorType') as errorType
|
JSONExtractString(payload, 'errorType') as errorType,
|
||||||
|
version
|
||||||
FROM event
|
FROM event
|
||||||
WHERE action = 'serverlessFunction.executed'
|
WHERE action = 'serverlessFunction.executed'
|
||||||
|
|
||||||
|
|||||||
@ -132,6 +132,7 @@ export {
|
|||||||
IconFocusCentered,
|
IconFocusCentered,
|
||||||
IconForbid,
|
IconForbid,
|
||||||
IconFunction,
|
IconFunction,
|
||||||
|
IconGauge,
|
||||||
IconGitCommit,
|
IconGitCommit,
|
||||||
IconGripVertical,
|
IconGripVertical,
|
||||||
IconH1,
|
IconH1,
|
||||||
|
|||||||
@ -29,4 +29,4 @@
|
|||||||
"path": "./tsconfig.storybook.json"
|
"path": "./tsconfig.storybook.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user