Sentry + Nuxt upgrade & (#738)
Implement Enhanced Validation Logging in UserFormRequest - Added a `failedValidation` method in `UserFormRequest` to log validation errors, including request data and user information, to the combined log channel and Slack. - Updated `nuxt.config.ts` to integrate the latest Sentry module and adjusted configurations for improved error tracking. - Refactored the `FeatureBase.vue` component to include error handling during user setup. - Removed the deprecated Sentry plugin and replaced it with the new Sentry Nuxt integration for better performance and maintainability. These changes aim to enhance error tracking and improve the overall robustness of form validation and user experience.
This commit is contained in:
parent
3dd3147b19
commit
7efa8ed9cb
|
|
@ -5,6 +5,9 @@ namespace App\Http\Requests;
|
|||
use App\Http\Requests\Workspace\CustomDomainRequest;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Rules\FormPropertyLogicRule;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
|
|
@ -30,6 +33,37 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
|
|||
$this->merge($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a failed validation attempt.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Validation\Validator $validator
|
||||
* @return void
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
protected function failedValidation(Validator $validator)
|
||||
{
|
||||
// Log validation errors to default log and Slack
|
||||
$errors = $validator->errors()->toArray();
|
||||
$requestData = $this->except(['password']); // Exclude sensitive data
|
||||
|
||||
$logData = [
|
||||
'errors' => $errors,
|
||||
'request_data' => $requestData,
|
||||
'ip' => request()->ip(),
|
||||
'user_agent' => request()->userAgent(),
|
||||
'route' => request()->route()->getName() ?? request()->path()
|
||||
];
|
||||
|
||||
// Log to both default channel and Slack
|
||||
Log::channel('combined')->warning(
|
||||
'Frontend validation bypass detected in form submission',
|
||||
$logData
|
||||
);
|
||||
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -68,14 +68,22 @@ onMounted(() => {
|
|||
|
||||
if (!user.value) return
|
||||
loadScript()
|
||||
setupForUser()
|
||||
try {
|
||||
setupForUser()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
watch(user, (val) => {
|
||||
if (import.meta.server || !val) return
|
||||
|
||||
loadScript()
|
||||
setupForUser()
|
||||
try {
|
||||
setupForUser()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,133 +1,142 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import runtimeConfig from "./runtimeConfig"
|
||||
import {sentryVitePlugin} from "@sentry/vite-plugin"
|
||||
import sitemap from "./sitemap"
|
||||
import gtm from "./gtm"
|
||||
|
||||
export default defineNuxtConfig({
|
||||
loglevel: process.env.NUXT_LOG_LEVEL || 'info',
|
||||
devtools: {enabled: true},
|
||||
css: ['~/scss/app.scss'],
|
||||
modules: [
|
||||
'@pinia/nuxt',
|
||||
'@vueuse/nuxt',
|
||||
'@vueuse/motion/nuxt',
|
||||
'@nuxtjs/sitemap',
|
||||
'@nuxt/ui',
|
||||
'nuxt-utm',
|
||||
'@nuxtjs/i18n',
|
||||
'@nuxt/icon',
|
||||
...process.env.NUXT_PUBLIC_GTM_CODE ? ['@zadigetvoltaire/nuxt-gtm'] : [],
|
||||
],
|
||||
build: {
|
||||
transpile: ["vue-notion", "query-builder-vue-3", "vue-signature-pad"],
|
||||
},
|
||||
i18n: {
|
||||
locales: [
|
||||
{ code: 'en', name: 'English', iso: 'en-US', file: 'en.json' },
|
||||
{ code: 'fr', name: 'French', iso: 'fr-FR', file: 'fr.json' },
|
||||
{ code: 'hi', name: 'Hindi', iso: 'hi-IN', file: 'hi.json' },
|
||||
{ code: 'es', name: 'Spanish', iso: 'es-ES', file: 'es.json' },
|
||||
{ code: 'ar', name: 'Arabic', iso: 'ar-EG', file: 'ar.json' },
|
||||
{ code: 'zh', name: 'Chinese', iso: 'zh-CN', file: 'zh.json' },
|
||||
{ code: 'ja', name: 'Japanese', iso: 'ja-JP', file: 'ja.json' },
|
||||
{ code: 'bn', name: 'Bengali', iso: 'bn-BD', file: 'bn.json' },
|
||||
{ code: 'pt', name: 'Portuguese', iso: 'pt-BR', file: 'pt.json' },
|
||||
{ code: 'ru', name: 'Russian', iso: 'ru-RU', file: 'ru.json' },
|
||||
{ code: 'ur', name: 'Urdu', iso: 'ur-PK', file: 'ur.json' },
|
||||
{ code: 'pa', name: 'Punjabi', iso: 'pa-IN', file: 'pa.json' },
|
||||
{ code: 'de', name: 'German', iso: 'de-DE', file: 'de.json' },
|
||||
{ code: 'jv', name: 'Javanese', iso: 'jv-ID', file: 'jv.json' },
|
||||
{ code: 'ko', name: 'Korean', iso: 'ko-KR', file: 'ko.json' },
|
||||
{ code: 'vi', name: 'Vietnamese', iso: 'vi-VN', file: 'vi.json' },
|
||||
{ code: 'te', name: 'Telugu', iso: 'te-IN', file: 'te.json' },
|
||||
{ code: 'mr', name: 'Marathi', iso: 'mr-IN', file: 'mr.json' },
|
||||
{ code: 'ta', name: 'Tamil', iso: 'ta-IN', file: 'ta.json' },
|
||||
{ code: 'tr', name: 'Turkish', iso: 'tr-TR', file: 'tr.json' },
|
||||
{ code: 'sk', name: 'Slovak', iso: 'sk-SK', file: 'sk.json' },
|
||||
{ code: 'cs', name: 'Czech', iso: 'cs-CZ', file: 'cs.json' },
|
||||
{ code: 'eu', name: 'Basque', iso: 'eu-ES', file: 'eu.json' },
|
||||
{ code: 'gl', name: 'Galician', iso: 'gl-ES', file: 'gl.json' },
|
||||
{ code: 'ca', name: 'Valencian/Catalan', iso: 'ca-ES', file: 'ca.json' },
|
||||
{ code: 'sv', name: 'Swedish', iso: 'sv-SE', file: 'sv.json' },
|
||||
{ code: 'pl', name: 'Polish', iso: 'pl-PL', file: 'pl.json' },
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
lazy: true,
|
||||
langDir: 'lang/',
|
||||
strategy: 'no_prefix',
|
||||
detectBrowserLanguage: {
|
||||
cookieSecure: true
|
||||
}
|
||||
},
|
||||
experimental: {
|
||||
inlineRouteRules: true
|
||||
},
|
||||
sentry: {
|
||||
dsn: process.env.NUXT_PUBLIC_SENTRY_DSN,
|
||||
lazy: true,
|
||||
},
|
||||
gtag: {
|
||||
id: process.env.NUXT_PUBLIC_GOOGLE_ANALYTICS_CODE,
|
||||
},
|
||||
components: [
|
||||
{
|
||||
path: '~/components/forms',
|
||||
pathPrefix: false,
|
||||
global: true,
|
||||
},
|
||||
{
|
||||
path: '~/components/global',
|
||||
pathPrefix: false,
|
||||
},
|
||||
{
|
||||
path: '~/components/forms',
|
||||
pathPrefix: false,
|
||||
global: true
|
||||
},
|
||||
{
|
||||
path: '~/components/pages',
|
||||
pathPrefix: false,
|
||||
},
|
||||
{
|
||||
path: '~/components/open/integrations',
|
||||
pathPrefix: false,
|
||||
global: true,
|
||||
},
|
||||
'~/components',
|
||||
],
|
||||
sourcemap: true,
|
||||
vite: {
|
||||
plugins: [
|
||||
// Put the Sentry vite plugin after all other plugins
|
||||
sentryVitePlugin({
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
org: "opnform",
|
||||
project: "opnform-vue",
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
hmr: {
|
||||
clientPort: 3000
|
||||
}
|
||||
}
|
||||
},
|
||||
tailwindcss: {
|
||||
cssPath: ['~/scss/app.scss']
|
||||
},
|
||||
colorMode: {
|
||||
preference: 'light',
|
||||
fallback: 'light',
|
||||
classPrefix: '',
|
||||
},
|
||||
icon: {
|
||||
clientBundle: {
|
||||
scan: {
|
||||
globInclude: ['**/*.vue', '**/*.json'],
|
||||
},
|
||||
},
|
||||
loglevel: process.env.NUXT_LOG_LEVEL || 'info',
|
||||
devtools: {enabled: true},
|
||||
css: ['~/scss/app.scss'],
|
||||
|
||||
modules: [
|
||||
'@pinia/nuxt',
|
||||
'@vueuse/nuxt',
|
||||
'@vueuse/motion/nuxt',
|
||||
'@nuxtjs/sitemap',
|
||||
'@nuxt/ui',
|
||||
'nuxt-utm',
|
||||
'@nuxtjs/i18n',
|
||||
'@nuxt/icon',
|
||||
'@sentry/nuxt/module',
|
||||
...(process.env.NUXT_PUBLIC_GTM_CODE ? ['@zadigetvoltaire/nuxt-gtm'] : []),
|
||||
],
|
||||
|
||||
build: {
|
||||
transpile: ["vue-notion", "query-builder-vue-3", "vue-signature-pad"],
|
||||
},
|
||||
|
||||
i18n: {
|
||||
locales: [
|
||||
{ code: 'en', name: 'English', iso: 'en-US', file: 'en.json' },
|
||||
{ code: 'fr', name: 'French', iso: 'fr-FR', file: 'fr.json' },
|
||||
{ code: 'hi', name: 'Hindi', iso: 'hi-IN', file: 'hi.json' },
|
||||
{ code: 'es', name: 'Spanish', iso: 'es-ES', file: 'es.json' },
|
||||
{ code: 'ar', name: 'Arabic', iso: 'ar-EG', file: 'ar.json' },
|
||||
{ code: 'zh', name: 'Chinese', iso: 'zh-CN', file: 'zh.json' },
|
||||
{ code: 'ja', name: 'Japanese', iso: 'ja-JP', file: 'ja.json' },
|
||||
{ code: 'bn', name: 'Bengali', iso: 'bn-BD', file: 'bn.json' },
|
||||
{ code: 'pt', name: 'Portuguese', iso: 'pt-BR', file: 'pt.json' },
|
||||
{ code: 'ru', name: 'Russian', iso: 'ru-RU', file: 'ru.json' },
|
||||
{ code: 'ur', name: 'Urdu', iso: 'ur-PK', file: 'ur.json' },
|
||||
{ code: 'pa', name: 'Punjabi', iso: 'pa-IN', file: 'pa.json' },
|
||||
{ code: 'de', name: 'German', iso: 'de-DE', file: 'de.json' },
|
||||
{ code: 'jv', name: 'Javanese', iso: 'jv-ID', file: 'jv.json' },
|
||||
{ code: 'ko', name: 'Korean', iso: 'ko-KR', file: 'ko.json' },
|
||||
{ code: 'vi', name: 'Vietnamese', iso: 'vi-VN', file: 'vi.json' },
|
||||
{ code: 'te', name: 'Telugu', iso: 'te-IN', file: 'te.json' },
|
||||
{ code: 'mr', name: 'Marathi', iso: 'mr-IN', file: 'mr.json' },
|
||||
{ code: 'ta', name: 'Tamil', iso: 'ta-IN', file: 'ta.json' },
|
||||
{ code: 'tr', name: 'Turkish', iso: 'tr-TR', file: 'tr.json' },
|
||||
{ code: 'sk', name: 'Slovak', iso: 'sk-SK', file: 'sk.json' },
|
||||
{ code: 'cs', name: 'Czech', iso: 'cs-CZ', file: 'cs.json' },
|
||||
{ code: 'eu', name: 'Basque', iso: 'eu-ES', file: 'eu.json' },
|
||||
{ code: 'gl', name: 'Galician', iso: 'gl-ES', file: 'gl.json' },
|
||||
{ code: 'ca', name: 'Valencian/Catalan', iso: 'ca-ES', file: 'ca.json' },
|
||||
{ code: 'sv', name: 'Swedish', iso: 'sv-SE', file: 'sv.json' },
|
||||
{ code: 'pl', name: 'Polish', iso: 'pl-PL', file: 'pl.json' },
|
||||
],
|
||||
defaultLocale: 'en',
|
||||
lazy: true,
|
||||
langDir: 'lang/',
|
||||
strategy: 'no_prefix',
|
||||
detectBrowserLanguage: {
|
||||
cookieSecure: true
|
||||
}
|
||||
},
|
||||
|
||||
experimental: {
|
||||
inlineRouteRules: true
|
||||
},
|
||||
|
||||
sentry: {
|
||||
sourceMapsUploadOptions: {
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
org: "opnform",
|
||||
project: "opnform-vue",
|
||||
},
|
||||
sitemap,
|
||||
runtimeConfig,
|
||||
gtm
|
||||
},
|
||||
|
||||
sourcemap: { client: 'hidden' },
|
||||
|
||||
gtag: {
|
||||
id: process.env.NUXT_PUBLIC_GOOGLE_ANALYTICS_CODE,
|
||||
},
|
||||
|
||||
components: [
|
||||
{
|
||||
path: '~/components/forms',
|
||||
pathPrefix: false,
|
||||
global: true,
|
||||
},
|
||||
{
|
||||
path: '~/components/global',
|
||||
pathPrefix: false,
|
||||
},
|
||||
{
|
||||
path: '~/components/forms',
|
||||
pathPrefix: false,
|
||||
global: true
|
||||
},
|
||||
{
|
||||
path: '~/components/pages',
|
||||
pathPrefix: false,
|
||||
},
|
||||
{
|
||||
path: '~/components/open/integrations',
|
||||
pathPrefix: false,
|
||||
global: true,
|
||||
},
|
||||
'~/components',
|
||||
],
|
||||
|
||||
vite: {
|
||||
server: {
|
||||
hmr: {
|
||||
clientPort: 3000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
tailwindcss: {
|
||||
cssPath: ['~/scss/app.scss']
|
||||
},
|
||||
|
||||
colorMode: {
|
||||
preference: 'light',
|
||||
fallback: 'light',
|
||||
classPrefix: '',
|
||||
},
|
||||
|
||||
icon: {
|
||||
clientBundle: {
|
||||
scan: {
|
||||
globInclude: ['**/*.vue', '**/*.json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sitemap,
|
||||
runtimeConfig,
|
||||
gtm,
|
||||
compatibilityDate: '2024-10-30'
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -26,7 +26,7 @@
|
|||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"nuxt": "^3.14.1592",
|
||||
"nuxt": "^3.16.1",
|
||||
"nuxt-utm": "^0.2.2",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
|
|
@ -40,8 +40,9 @@
|
|||
"@nuxt/ui": "^2.19.2",
|
||||
"@pinia/nuxt": "^0.5.5",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/vite-plugin": "^2.22.6",
|
||||
"@sentry/vue": "^7.119.2",
|
||||
"@sentry/nuxt": "^9.8.0",
|
||||
"@sentry/vite-plugin": "^3.2.2",
|
||||
"@sentry/vue": "^9.8.0",
|
||||
"@vueuse/components": "^11.2.0",
|
||||
"@vueuse/core": "^11.2.0",
|
||||
"@vueuse/integrations": "^11.2.0",
|
||||
|
|
@ -80,5 +81,8 @@
|
|||
},
|
||||
"eslintIgnore": [
|
||||
"/lib/jaro-winkler.js"
|
||||
]
|
||||
],
|
||||
"overrides": {
|
||||
"@vercel/nft": "^0.27.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
import * as Sentry from "@sentry/vue"
|
||||
|
||||
function getSentryIntegrations() {
|
||||
// don't load on server
|
||||
if (!import.meta.client) return []
|
||||
|
||||
const router = useRouter()
|
||||
const browserTracing = new Sentry.BrowserTracing({
|
||||
routingInstrumentation: Sentry.vueRouterInstrumentation(router),
|
||||
})
|
||||
|
||||
return [browserTracing]
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: "sentry",
|
||||
parallel: true,
|
||||
async setup(nuxtApp) {
|
||||
const vueApp = nuxtApp.vueApp
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
Sentry.init({
|
||||
app: vueApp,
|
||||
dsn: config.public.SENTRY_DSN_PUBLIC ?? null,
|
||||
integrations: getSentryIntegrations(),
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: config.public.SENTRY_TRACES_SAMPLE_RATE,
|
||||
|
||||
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
|
||||
// tracePropagationTargets: ["localhost", /^https:\/\/yourserver\.io\/api/],
|
||||
|
||||
// This sets the sample rate. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: config.public.SENTRY_REPLAY_SAMPLE_RATE,
|
||||
|
||||
// If the entire session is not sampled, use the below sample rate to sample
|
||||
// sessions when an error occurs.
|
||||
replaysOnErrorSampleRate: config.public.SENTRY_ERROR_REPLAY_SAMPLE_RATE,
|
||||
|
||||
beforeSend(event) {
|
||||
if (event.exception.values.length) {
|
||||
// Don't send validation exceptions to Sentry
|
||||
if (
|
||||
event.exception.values[0].type === "FetchError" &&
|
||||
(event.exception.values[0].value.includes("422") ||
|
||||
event.exception.values[0].value.includes("401"))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return event
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import * as Sentry from "@sentry/nuxt";
|
||||
|
||||
Sentry.init({
|
||||
// If set up, you can use your runtime config here
|
||||
// dsn: useRuntimeConfig().public.sentry.dsn,
|
||||
dsn: useRuntimeConfig().public.SENTRY_DSN_PUBLIC ?? null,
|
||||
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// If the entire session is not sampled, use the below sample rate to sample
|
||||
// sessions when an error occurs.
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
// If you don't want to use Session Replay, just remove the line below:
|
||||
integrations: [Sentry.replayIntegration()],
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
// Ensure that source maps are properly handled
|
||||
attachStacktrace: true,
|
||||
|
||||
beforeSend (event) {
|
||||
if (event.exception?.values?.length) {
|
||||
// Don't send validation exceptions to Sentry
|
||||
if (
|
||||
event.exception.values[0]?.type === 'FetchError'
|
||||
&& (event.exception.values[0]?.value?.includes('422')
|
||||
|| event.exception.values[0]?.value?.includes('401'))
|
||||
)
|
||||
return null
|
||||
}
|
||||
const authStore = useAuthStore()
|
||||
if (authStore.check) {
|
||||
Sentry.setUser({
|
||||
id: authStore.user?.id,
|
||||
email: authStore.user?.email
|
||||
})
|
||||
event.user = {
|
||||
id: authStore.user?.id,
|
||||
email: authStore.user?.email
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import * as Sentry from "@sentry/nuxt";
|
||||
import { useAuthStore } from "~/stores/auth";
|
||||
|
||||
Sentry.init({
|
||||
dsn: useRuntimeConfig().public.SENTRY_DSN_PUBLIC ?? null,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
});
|
||||
Loading…
Reference in New Issue