GH-2829 Add Sentry on frontend (#3111)

* GH-2829 pass sentry dsn key from backend in ClientConfig

* GH-2829 add Sentry library on frontend

* GH-2829 fetch dsnKey in GQL and add a state

* GH-2829 initialize Sentry on frontend

* GH-2829 fix linting issues

* Update yarn.lock

* GH-2829 update graphql schema for clientConfig

* GH-2829 remove Sentry comments

* GH-2829 rename sentry state

* GH-2829 rename dsnKey to dsn

* GH-2829 refactor to use componentEffect for sentry initialization

* GH-2829 fix linting issues

* GH-2829 update Graphql types

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Deepak Kumar
2023-12-22 04:20:24 +05:30
committed by GitHub
parent 756b30815e
commit 46ab88cb9c
13 changed files with 230 additions and 50 deletions

View File

@ -47,6 +47,7 @@
"@floating-ui/react": "^0.24.3",
"@hello-pangea/dnd": "^16.2.0",
"@hookform/resolvers": "^3.1.1",
"@sentry/react": "^7.88.0",
"@sniptt/guards": "^0.2.0",
"@swc/core": "^1.3.100",
"@swc/jest": "^0.2.29",

View File

@ -70,6 +70,7 @@ export type ClientConfig = {
authProviders: AuthProviders;
billing: Billing;
debugMode: Scalars['Boolean'];
sentry: Sentry;
signInPrefilled: Scalars['Boolean'];
support: Support;
telemetry: Telemetry;
@ -174,6 +175,12 @@ export enum FileFolder {
WorkspaceLogo = 'WorkspaceLogo'
}
export type FullName = {
__typename?: 'FullName';
firstName: Scalars['String'];
lastName: Scalars['String'];
};
export type IdFilterComparison = {
eq?: InputMaybe<Scalars['ID']>;
gt?: InputMaybe<Scalars['ID']>;
@ -426,6 +433,11 @@ export enum RelationMetadataType {
OneToOne = 'ONE_TO_ONE'
}
export type Sentry = {
__typename?: 'Sentry';
dsn: Scalars['String'];
};
/** Sort Directions */
export enum SortDirection {
Asc = 'ASC',
@ -489,7 +501,7 @@ export type User = {
passwordHash?: Maybe<Scalars['String']>;
supportUserHash?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
workspaceMember: UserWorkspaceMember;
workspaceMember: WorkspaceMember;
};
export type UserEdge = {
@ -505,21 +517,6 @@ export type UserExists = {
exists: Scalars['Boolean'];
};
export type UserWorkspaceMember = {
__typename?: 'UserWorkspaceMember';
avatarUrl?: Maybe<Scalars['String']>;
colorScheme: Scalars['String'];
id: Scalars['ID'];
locale: Scalars['String'];
name: UserWorkspaceMemberName;
};
export type UserWorkspaceMemberName = {
__typename?: 'UserWorkspaceMemberName';
firstName: Scalars['String'];
lastName: Scalars['String'];
};
export type Verify = {
__typename?: 'Verify';
tokens: AuthTokenPair;
@ -560,6 +557,15 @@ export type WorkspaceInviteHashValid = {
isValid: Scalars['Boolean'];
};
export type WorkspaceMember = {
__typename?: 'WorkspaceMember';
avatarUrl?: Maybe<Scalars['String']>;
colorScheme: Scalars['String'];
id: Scalars['ID'];
locale: Scalars['String'];
name: FullName;
};
export type Field = {
__typename?: 'field';
createdAt: Scalars['DateTime'];
@ -705,7 +711,7 @@ export type ImpersonateMutationVariables = Exact<{
}>;
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | 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: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{
refreshToken: Scalars['String'];
@ -728,7 +734,7 @@ export type VerifyMutationVariables = Exact<{
}>;
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | 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: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type CheckUserExistsQueryVariables = Exact<{
email: Scalars['String'];
@ -740,7 +746,7 @@ export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl: string }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null } } };
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl: string }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn: string } } };
export type UploadFileMutationVariables = Exact<{
file: Scalars['Upload'];
@ -758,7 +764,7 @@ export type UploadImageMutationVariables = Exact<{
export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: string };
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } };
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@ -775,7 +781,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } };
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } };
export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>;
@ -1284,6 +1290,9 @@ export const GetClientConfigDocument = gql`
supportDriver
supportFrontChatId
}
sentry {
dsn
}
}
}
`;

View File

@ -8,6 +8,7 @@ import { ApolloProvider } from '@/apollo/components/ApolloProvider';
import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider';
import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver';
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
@ -36,31 +37,33 @@ root.render(
<BrowserRouter>
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
<IconsProvider>
<ApolloProvider>
<HelmetProvider>
<ClientConfigProvider>
<UserProvider>
<ApolloMetadataClientProvider>
<ObjectMetadataItemsProvider>
<AppThemeProvider>
<SnackBarProvider>
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManager>
<StrictMode>
<PromiseRejectionEffect />
<App />
</StrictMode>
</DialogManager>
</DialogManagerScope>
</SnackBarProvider>
</AppThemeProvider>
<PageChangeEffect />
</ObjectMetadataItemsProvider>
</ApolloMetadataClientProvider>
</UserProvider>
</ClientConfigProvider>
</HelmetProvider>
</ApolloProvider>
<ExceptionHandlerProvider>
<ApolloProvider>
<HelmetProvider>
<ClientConfigProvider>
<UserProvider>
<ApolloMetadataClientProvider>
<ObjectMetadataItemsProvider>
<AppThemeProvider>
<SnackBarProvider>
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<DialogManager>
<StrictMode>
<PromiseRejectionEffect />
<App />
</StrictMode>
</DialogManager>
</DialogManagerScope>
</SnackBarProvider>
</AppThemeProvider>
<PageChangeEffect />
</ObjectMetadataItemsProvider>
</ApolloMetadataClientProvider>
</UserProvider>
</ClientConfigProvider>
</HelmetProvider>
</ApolloProvider>
</ExceptionHandlerProvider>
</IconsProvider>
</SnackBarProviderScope>
</BrowserRouter>

View File

@ -5,6 +5,7 @@ import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { telemetryState } from '@/client-config/states/telemetryState';
import { useGetClientConfigQuery } from '~/generated/graphql';
@ -21,6 +22,8 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
const setTelemetry = useSetRecoilState(telemetryState);
const setSupportChat = useSetRecoilState(supportChatState);
const setSentryConfig = useSetRecoilState(sentryConfigState);
const { data, loading } = useGetClientConfigQuery();
useEffect(() => {
@ -36,6 +39,10 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
setBilling(data?.clientConfig.billing);
setTelemetry(data?.clientConfig.telemetry);
setSupportChat(data?.clientConfig.support);
setSentryConfig({
dsn: data?.clientConfig?.sentry?.dsn,
});
}
}, [
data,
@ -45,6 +52,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
setTelemetry,
setSupportChat,
setBilling,
setSentryConfig,
]);
return loading ? <></> : <>{children}</>;

View File

@ -21,6 +21,9 @@ export const GET_CLIENT_CONFIG = gql`
supportDriver
supportFrontChatId
}
sentry {
dsn
}
}
}
`;

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { Sentry } from '~/generated/graphql';
export const sentryConfigState = atom<Sentry | null>({
key: 'sentryConfigState',
default: null,
});

View File

@ -0,0 +1,12 @@
import { SentryInitEffect } from '@/error-handler/components/SentryInitiEffect';
export const ExceptionHandlerProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
return (
<>
<SentryInitEffect />
{children}
</>
);
};

View File

@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';
import * as Sentry from '@sentry/react';
import { useRecoilValue } from 'recoil';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
export const SentryInitEffect = () => {
const sentryConfig = useRecoilValue(sentryConfigState);
const [isSentryInitialized, setIsSentryInitialized] = useState(false);
useEffect(() => {
if (sentryConfig?.dsn && !isSentryInitialized) {
Sentry.init({
dsn: sentryConfig?.dsn,
integrations: [
new Sentry.BrowserTracing({
tracePropagationTargets: [
'localhost:3001',
REACT_APP_SERVER_BASE_URL,
],
}),
new Sentry.Replay(),
],
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
setIsSentryInitialized(true);
}
}, [sentryConfig, isSentryInitialized]);
return <></>;
};

View File

@ -39,6 +39,12 @@ class Support {
supportFrontChatId: string | undefined;
}
@ObjectType()
class Sentry {
@Field(() => String)
dsn: string | undefined;
}
@ObjectType()
export class ClientConfig {
@Field(() => AuthProviders, { nullable: false })
@ -58,4 +64,7 @@ export class ClientConfig {
@Field(() => Support)
support: Support;
@Field(() => Sentry)
sentry: Sentry;
}

View File

@ -31,6 +31,9 @@ export class ClientConfigResolver {
supportDriver: this.environmentService.getSupportDriver(),
supportFrontChatId: this.environmentService.getSupportFrontChatId(),
},
sentry: {
dsn: this.environmentService.getSentryDSN(),
},
};
return Promise.resolve(clientConfig);

View File

@ -1,6 +1,7 @@
import { Module } from "@nestjs/common";
import { ModuleRef } from "@nestjs/core";
import { FetchMessagesJob } from "src/workspace/messaging/jobs/fetch-messages.job";
import { Module } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { FetchMessagesJob } from 'src/workspace/messaging/jobs/fetch-messages.job';
@Module({
providers: [

View File

@ -2,4 +2,4 @@ export function getJobClassName(name: string): string {
const [, jobName] = name.split('.') ?? [];
return jobName ?? name;
}
}