Browse Source

refactor: GUI error handling (#9528)

* refactor: error reporting

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: handle ee part

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: coderabbit review comments

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: linting

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: remove duplicate error log

Signed-off-by: Pranav C <pranavxc@gmail.com>

* refactor: request type correction

Signed-off-by: Pranav C <pranavxc@gmail.com>

---------

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/9601/head
Pranav C 2 months ago committed by GitHub
parent
commit
21e1f2a9fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      packages/nc-gui/components/nc/ErrorBoundary.vue
  2. 1
      packages/nc-gui/composables/useGlobal/types.ts
  3. 2
      packages/nc-gui/nuxt-shim.d.ts
  4. 92
      packages/nc-gui/plugins/error-reporting.ts
  5. 13
      packages/nc-gui/plugins/sentry.ts
  6. 25
      packages/nocodb-sdk/src/lib/Api.ts
  7. 20
      packages/nocodb/src/controllers/utils.controller.ts
  8. 42
      packages/nocodb/src/schema/swagger.json
  9. 20
      packages/nocodb/src/services/utils.service.ts

3
packages/nc-gui/components/nc/ErrorBoundary.vue

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
// modified version of default NuxtErrorBoundary component - https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/nuxt-error-boundary.ts // modified version of default NuxtErrorBoundary component - https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/components/nuxt-error-boundary.ts
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import * as Sentry from '@sentry/vue'
const MESSAGE_KEY = 'ErrorMessageKey' const MESSAGE_KEY = 'ErrorMessageKey'
@ -54,7 +53,7 @@ export default {
}, 30000) }, 30000)
try { try {
Sentry.captureException(err) nuxtApp.$report(err)
} catch { } catch {
// ignore // ignore
} }

1
packages/nc-gui/composables/useGlobal/types.ts

@ -40,6 +40,7 @@ export interface AppInfo {
samlProviderName: string | null samlProviderName: string | null
giftUrl: string giftUrl: string
feedEnabled: boolean feedEnabled: boolean
sentryDSN: string
} }
export interface StoredState { export interface StoredState {

2
packages/nc-gui/nuxt-shim.d.ts vendored

@ -12,6 +12,8 @@ declare module '#app' {
} }
/** {@link import('./plugins/tele') Telemetry} Emit telemetry event */ /** {@link import('./plugins/tele') Telemetry} Emit telemetry event */
$e: (event: string, data?: any) => void $e: (event: string, data?: any) => void
/** {@link import('./plugins/report') Error reporting} Error reporting */
$report: (event: Error) => void
$state: UseGlobalReturn $state: UseGlobalReturn
$poller: { $poller: {
subscribe( subscribe(

92
packages/nc-gui/plugins/error-reporting.ts

@ -0,0 +1,92 @@
import * as Sentry from '@sentry/vue'
import type { Api } from 'nocodb-sdk'
class ErrorReporting {
errors: Error[] = []
// debounce error reporting to avoid sending multiple reports for the same error
private report = useDebounceFn(
() => {
try {
const errors = this.errors
// filter out duplicate errors and only include 2 lines of stack trace
.filter((error, index, self) => index === self.findIndex((t) => t.message === error.message))
.map((error) => ({
message: error.message,
stack: error.stack?.split('\n').slice(0, 2).join('\n'),
}))
this.errors = []
this.$api.utils.errorReport({ errors, extra: {} })
} catch {
// ignore
}
},
3000,
{
maxWait: 10000,
},
)
constructor(private $api: Api<unknown>) {}
// collect error to report later
collect(error: Error) {
this.errors.push(error)
// report errors after 3 seconds
this.report()
}
}
export default defineNuxtPlugin((nuxtApp) => {
if (isEeUI) {
nuxtApp.provide('report', function (error: Error) {
try {
Sentry.captureException(error)
} catch {
// ignore
}
})
return
}
const env = process.env.NODE_ENV === 'production' ? 'production' : 'development'
let isSentryConfigured = false
let isErrorReportingEnabled = false
let errorReporting: ErrorReporting | null = null
// load error reporting only if enabled and sentryDSN is not provided
watch(
[
() => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.errorReportingEnabled,
() => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.sentryDSN,
],
([enabled, sentryDSN]) => {
isSentryConfigured = enabled && !!sentryDSN
isErrorReportingEnabled = enabled
if (enabled && !sentryDSN) {
errorReporting = new ErrorReporting(nuxtApp.$api as Api<unknown>)
} else {
errorReporting = null
}
},
{ immediate: true },
)
function report(error: Error) {
if (process.env.CI || process.env.PLAYWRIGHT) {
return
}
if (env !== 'production' && !process.env.NC_ENABLE_DEV_SENTRY) {
return
}
if (isSentryConfigured) {
Sentry.captureException(error)
} else if (isErrorReportingEnabled) {
errorReporting?.collect(error)
}
}
nuxtApp.provide('report', report)
})

13
packages/nc-gui/plugins/sentry.ts

@ -20,14 +20,14 @@ export default defineNuxtPlugin((nuxtApp) => {
let initialized = false let initialized = false
const init = () => { const init = (dsn: string) => {
// prevent multiple init // prevent multiple init
if (initialized) return if (initialized) return
initialized = true initialized = true
Sentry.init({ Sentry.init({
app: [vueApp], app: [vueApp],
dsn: 'https://64cb4904bcbec03a1b9a0be02a2d10a9@o4505953073889280.ingest.us.sentry.io/4507725383663616', dsn,
environment: env, environment: env,
integrations: [ integrations: [
new Sentry.BrowserTracing({ new Sentry.BrowserTracing({
@ -56,11 +56,14 @@ export default defineNuxtPlugin((nuxtApp) => {
// load sentry only if enabled // load sentry only if enabled
watch( watch(
[
() => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.errorReportingEnabled, () => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.errorReportingEnabled,
(enabled) => { () => (nuxtApp.$state as ReturnType<typeof useGlobal>).appInfo?.value?.sentryDSN,
],
([enabled, sentryDSN]) => {
try { try {
if (enabled) init() if (enabled && sentryDSN) init(sentryDSN)
} catch (e) { } catch {
// ignore // ignore
} }
}, },

25
packages/nocodb-sdk/src/lib/Api.ts

@ -3100,6 +3100,14 @@ export interface CalendarColumnReqType {
order?: number; order?: number;
} }
export interface ErrorReportReqType {
errors?: {
message?: string;
stack?: string;
}[];
extra?: object;
}
/** /**
* Model for Comment * Model for Comment
*/ */
@ -10735,6 +10743,23 @@ export class Api<
}), }),
/** /**
* @description Error Reporting
*
* @tags Utils, Internal
* @name ErrorReport
* @summary Error Reporting
* @request POST:/api/v1/error-reporting
*/
errorReport: (data: any, params: RequestParams = {}) =>
this.request<any, any>({
path: `/api/v1/error-reporting`,
method: 'POST',
body: data,
type: ContentType.Json,
...params,
}),
/**
* @description Generic Axios Call * @description Generic Axios Call
* *
* @tags Utils * @tags Utils

20
packages/nocodb/src/controllers/utils.controller.ts

@ -12,6 +12,7 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ProjectRoles, validateAndExtractSSLProp } from 'nocodb-sdk'; import { ProjectRoles, validateAndExtractSSLProp } from 'nocodb-sdk';
import { import {
ErrorReportReqType,
getTestDatabaseName, getTestDatabaseName,
IntegrationsType, IntegrationsType,
OrgUserRoles, OrgUserRoles,
@ -26,7 +27,7 @@ import { NcRequest } from '~/interface/config';
import { Integration } from '~/models'; import { Integration } from '~/models';
import { MetaTable, RootScopes } from '~/utils/globals'; import { MetaTable, RootScopes } from '~/utils/globals';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
import { deepMerge } from '~/utils'; import { deepMerge, isEE } from '~/utils';
import Noco from '~/Noco'; import Noco from '~/Noco';
@Controller() @Controller()
@ -173,4 +174,21 @@ export class UtilsController {
async feed(@Request() req: NcRequest) { async feed(@Request() req: NcRequest) {
return await this.utilsService.feed(req); return await this.utilsService.feed(req);
} }
@UseGuards(PublicApiLimiterGuard)
@Post('/api/v1/error-reporting')
async reportErrors(@Req() req: NcRequest, @Body() body: ErrorReportReqType) {
if (
`${process.env.NC_DISABLE_ERR_REPORTS}` === 'true' ||
isEE ||
process.env.NC_SENTRY_DSN
) {
return {};
}
return (await this.utilsService.reportErrors({
req,
body,
})) as any;
}
} }

42
packages/nocodb/src/schema/swagger.json

@ -15843,6 +15843,26 @@
] ]
} }
}, },
"/api/v1/error-reporting": {
"post": {
"summary": "Error Reporting",
"operationId": "utils-error-report",
"responses": {
},
"description": "Error Reporting",
"tags": [
"Utils",
"Internal"
],
"requestBody": {
"content": {
"application/json": {
"$ref": "#/components/schemas/ErrorReportReq"
}
}
}
}
},
"/api/v1/db/meta/axiosRequestMake": { "/api/v1/db/meta/axiosRequestMake": {
"parameters": [ "parameters": [
{ {
@ -27014,6 +27034,28 @@
"id": "psbv6c6y9qvbu" "id": "psbv6c6y9qvbu"
} }
}, },
"ErrorReportReq": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"stack": {
"type": "string"
}
}
}
},
"extra": {
"type": "object"
}
}
},
"Comment": { "Comment": {
"description": "Model for Comment", "description": "Model for Comment",
"type": "object", "type": "object",

20
packages/nocodb/src/services/utils.service.ts

@ -6,13 +6,14 @@ import { ViewTypes } from 'nocodb-sdk';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { useAgent } from 'request-filtering-agent'; import { useAgent } from 'request-filtering-agent';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import type { ErrorReportReqType } from 'nocodb-sdk';
import type { AppConfig, NcRequest } from '~/interface/config'; import type { AppConfig, NcRequest } from '~/interface/config';
import { T } from '~/utils';
import { NC_APP_SETTINGS, NC_ATTACHMENT_FIELD_SIZE } from '~/constants'; import { NC_APP_SETTINGS, NC_ATTACHMENT_FIELD_SIZE } from '~/constants';
import SqlMgrv2 from '~/db/sql-mgr/v2/SqlMgrv2'; import SqlMgrv2 from '~/db/sql-mgr/v2/SqlMgrv2';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
import { Base, Store, User } from '~/models'; import { Base, Store, User } from '~/models';
import Noco from '~/Noco'; import Noco from '~/Noco';
import { T } from '~/utils';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
import getInstance from '~/utils/getInstance'; import getInstance from '~/utils/getInstance';
import { CacheScope, MetaTable, RootScopes } from '~/utils/globals'; import { CacheScope, MetaTable, RootScopes } from '~/utils/globals';
@ -456,6 +457,10 @@ export class UtilsService {
ncMin: !!process.env.NC_MIN, ncMin: !!process.env.NC_MIN,
teleEnabled: process.env.NC_DISABLE_TELE !== 'true', teleEnabled: process.env.NC_DISABLE_TELE !== 'true',
errorReportingEnabled: process.env.NC_DISABLE_ERR_REPORTS !== 'true', errorReportingEnabled: process.env.NC_DISABLE_ERR_REPORTS !== 'true',
sentryDSN:
process.env.NC_DISABLE_ERR_REPORTS !== 'true'
? process.env.NC_SENTRY_DSN
: null,
auditEnabled: process.env.NC_DISABLE_AUDIT !== 'true', auditEnabled: process.env.NC_DISABLE_AUDIT !== 'true',
ncSiteUrl: (param.req as any).ncSiteUrl, ncSiteUrl: (param.req as any).ncSiteUrl,
ee: Noco.isEE(), ee: Noco.isEE(),
@ -480,6 +485,19 @@ export class UtilsService {
return result; return result;
} }
async reportErrors(param: { body: ErrorReportReqType; req: NcRequest }) {
for (const error of param.body?.errors ?? []) {
T.emit('evt', {
evt_type: 'gui:error',
properties: {
message: error.message,
stack: error.stack?.split('\n').slice(0, 2).join('\n'),
...(param.body.extra || {}),
},
});
}
}
async feed(req: NcRequest) { async feed(req: NcRequest) {
const { const {
type = 'all', type = 'all',

Loading…
Cancel
Save