Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 6 additions & 14 deletions apps/studio/components/interfaces/Home/ProjectList/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { DeleteProjectModal } from 'components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal'
import CardButton from 'components/ui/CardButton'
import { ComputeBadgeWrapper } from 'components/ui/ComputeBadgeWrapper'
import type { IntegrationProjectConnection } from 'data/integrations/integrations.types'
Expand All @@ -8,8 +7,8 @@ import type { ResourceWarning } from 'data/usage/resource-warnings-query'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { BASE_PATH } from 'lib/constants'
import { Copy, Github, MoreVertical, Trash } from 'lucide-react'
import { useState } from 'react'
import { Copy, Github, MoreVertical, Settings } from 'lucide-react'
import { useRouter } from 'next/router'
import InlineSVG from 'react-inlinesvg'
import { toast } from 'sonner'
import type { Organization } from 'types'
Expand Down Expand Up @@ -38,14 +37,13 @@ export interface ProjectCardProps {
export const ProjectCard = ({
slug,
project,
organization,
rewriteHref,
githubIntegration,
vercelIntegration,
resourceWarnings,
}: ProjectCardProps) => {
const router = useRouter()
const { name, ref: projectRef } = project
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)

const { infraAwsNimbusLabel } = useCustomContent(['infra:aws_nimbus_label'])
const providerLabel =
Expand Down Expand Up @@ -104,11 +102,11 @@ export const ProjectCard = ({
className="gap-x-2"
onClick={(e) => {
e.stopPropagation()
setIsDeleteModalOpen(true)
router.push(`/project/${projectRef}/settings/general`)
}}
>
<Trash size={14} />
<span>Delete project</span>
<Settings size={14} />
<span>Settings</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand Down Expand Up @@ -156,12 +154,6 @@ export const ProjectCard = ({
containerElement={<ProjectIndexPageLink projectRef={projectRef} />}
/>
</li>
<DeleteProjectModal
visible={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
project={project}
organization={organization}
/>
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Github, MoreVertical, Trash, Copy, Check } from 'lucide-react'
import { Github, MoreVertical, Settings, Copy, Check } from 'lucide-react'
import { useRouter } from 'next/router'
import InlineSVG from 'react-inlinesvg'
import { useState } from 'react'
Expand All @@ -22,7 +22,6 @@ import {
import { TimestampInfo } from 'ui-patterns'
import { inferProjectStatus } from './ProjectCard.utils'
import { ProjectCardStatus } from './ProjectCardStatus'
import { DeleteProjectModal } from 'components/interfaces/Settings/General/DeleteProjectPanel/DeleteProjectModal'
import { toast } from 'sonner'
import { copyToClipboard } from 'ui'

Expand All @@ -46,7 +45,6 @@ export const ProjectTableRow = ({
const router = useRouter()
const { name, ref: projectRef } = project
const projectStatus = inferProjectStatus(project.status)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isCopied, setIsCopied] = useState(false)

const url = rewriteHref ?? `/project/${project.ref}`
Expand Down Expand Up @@ -177,23 +175,17 @@ export const ProjectTableRow = ({
className="gap-x-2"
onClick={(e) => {
e.stopPropagation()
setIsDeleteModalOpen(true)
router.push(`/project/${projectRef}/settings/general`)
}}
>
<Trash size={14} />
<span>Delete project</span>
<Settings size={14} />
<span>Settings</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
<DeleteProjectModal
visible={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
project={project}
organization={organization}
/>
</>
)
}
201 changes: 115 additions & 86 deletions apps/studio/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import { IS_PLATFORM } from 'common/constants/environment'
import { MIRRORED_BREADCRUMBS } from 'lib/breadcrumbs'
import { sanitizeArrayOfObjects, sanitizeUrlHashParams } from 'lib/sanitize'

const DEFAULT_ERROR_SAMPLE_RATE = 1.0
const LOW_PRIORITY_ERROR_SAMPLE_RATE = 0.01
const CHUNK_LOAD_ERROR_PATTERNS = [
/ChunkLoadError/i,
/Loading chunk [\d]+ failed/i,
/Loading CSS chunk [\d]+ failed/i,
]

// This is a workaround to ignore hCaptcha related errors.
function isHCaptchaRelatedError(event: Sentry.Event): boolean {
const errors = event.exception?.values ?? []
Expand All @@ -22,32 +30,6 @@ function isHCaptchaRelatedError(event: Sentry.Event): boolean {
return false
}

// We want to ignore errors not originating from docs app static files
// (such as errors from browser extensions). Those errors come from files
// not starting with 'app:///_next'.
//
// However, there is a complication because the Sentry code that sends
// the error shows up in the stack trace, and that _does_ start with
// 'app:///_next'. It is always the first frame in the stack trace,
// and has a specific pre_context comment that we can use for filtering.
// Copied from docs app instrumentation-client.ts
function isThirdPartyError(frames: Sentry.StackFrame[] | undefined) {
if (!frames || frames.length === 0) return false

function isSentryFrame(frame: Sentry.StackFrame, index: number) {
return index === 0 && frame.pre_context?.some((line) => line.includes('sentry.javascript'))
}

// Check if any frame is from our app (excluding Sentry's own frame)
const hasAppFrame = frames.some((frame, index) => {
const path = frame.abs_path || frame.filename
return path?.startsWith('app:///_next') && !isSentryFrame(frame, index)
})

// If no app frames found, it's a third-party error
return !hasAppFrame
}

// Filter browser wallet extension errors (e.g., Gate.io wallet)
// These errors come from injected wallet scripts and are not actionable
// Examples: SUPABASE-APP-AFC, SUPABASE-APP-92A
Expand Down Expand Up @@ -94,6 +76,17 @@ export function isChallengeExpiredError(error: unknown, event: Sentry.Event): bo
return message.includes('challenge-expired')
}

function isChunkLoadError(error: unknown, event: Sentry.Event): boolean {
const errorMessage = error instanceof Error ? error.message : ''
const eventMessage = event.message || ''
const exceptionMessages = event.exception?.values?.map((ex) => ex.value ?? '') ?? []
const combinedMessages = [errorMessage, eventMessage, ...exceptionMessages].filter(Boolean)

return CHUNK_LOAD_ERROR_PATTERNS.some((pattern) =>
combinedMessages.some((message) => pattern.test(message))
)
}

Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
...(process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT && {
Expand All @@ -102,12 +95,31 @@ Sentry.init({
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,

// Enable performance monitoring - Next.js routes and API calls are automatically instrumented
// Enable performance monitoring
tracesSampleRate: 0.001, // Capture 0.1% of transactions for performance monitoring

// [Ali] Filter out browser extensions and user scripts (FE-2094)
// Using denyUrls to block known third-party script patterns
denyUrls: [/userscript/i],
integrations: (() => {
const thirdPartyErrorFilterIntegration = (Sentry as any).thirdPartyErrorFilterIntegration
if (!thirdPartyErrorFilterIntegration) return []

// Drop errors whose stack trace only contains third-party frames (browser extensions,
// injected scripts, etc.). This uses build-time code annotation via the applicationKey
// in next.config.js to reliably distinguish our code from third-party code.
return [
thirdPartyErrorFilterIntegration({
filterKeys: ['supabase-studio'],
behaviour: 'drop-error-if-exclusively-contains-third-party-frames',
}),
]
})(),

// Only capture errors originating from our own code.
// This is a whitelist on the source URL in stack frames — it drops errors from
// browser extensions, injected scripts, third-party widgets, etc. (FE-2094)
allowUrls: [
/https?:\/\/(.*\.)?supabase\.(com|co|green|io)/,
/app:\/\//, // Next.js rewrites source URLs to app:// with source maps
],
beforeBreadcrumb(breadcrumb, _hint) {
const cleanedBreadcrumb = { ...breadcrumb }

Expand All @@ -134,25 +146,36 @@ Sentry.init({
return null
}

// Ignore invalid URL events for 99% of the time because it's using up a lot of quota.
// Downsample only known high-noise classes; keep all other errors at full rate.
const isInvalidUrlEvent = (hint.originalException as any)?.message?.includes(
`Failed to construct 'URL': Invalid URL`
)
// [Joshen] Similar behaviour for this error from SessionTimeoutModal to control the quota usage
const isSessionTimeoutEvent = (hint.originalException as any)?.message?.includes(
'Session error detected'
)
const isChunkLoadFailure = isChunkLoadError(hint.originalException, event)

const codeSampleRate =
isInvalidUrlEvent || isSessionTimeoutEvent || isChunkLoadFailure
? LOW_PRIORITY_ERROR_SAMPLE_RATE
: DEFAULT_ERROR_SAMPLE_RATE

if ((isInvalidUrlEvent || isSessionTimeoutEvent) && Math.random() > 0.01) {
if (Math.random() > codeSampleRate) {
return null
}

event.tags = {
...event.tags,
codeSampleRate: codeSampleRate.toString(),
}

if (isHCaptchaRelatedError(event)) {
return null
}

const frames = event.exception?.values?.[0].stacktrace?.frames || []
if (isThirdPartyError(frames)) {
// Drop events where every exception has no stack trace — these are not debuggable
const exceptions = event.exception?.values ?? []
if (exceptions.length > 0 && exceptions.every((ex) => !ex.stacktrace?.frames?.length)) {
return null
}

Expand Down Expand Up @@ -183,73 +206,79 @@ Sentry.init({
return event
},
ignoreErrors: [
// Used exclusively in Monaco Editor.
// === Monaco Editor ===
'ResizeObserver',
's.getModifierState is not a function',
/^Uncaught NetworkError: Failed to execute 'importScripts' on 'WorkerGlobalScope'/,
// Browser wallet extension errors (e.g., Gate.io wallet)

// === Browser extension errors ===
// Gate.io wallet
'shouldSetTallyForCurrentProvider is not a function',
// [Joshen] We currently use stripe-js for customers to save their credit card data
// I'm unable to reproduce this error on local, staging nor prod across chrome, safari or firefox
// Based on https://github.com/stripe/stripe-js/issues/26, it seems like this error is safe to ignore,
// SAP browser extensions (SAP GUI, SAP Companion)
'sap is not defined',
// Non-Error objects thrown as exceptions (e.g., Event objects)
'[object Event]',

// === Third-party SDK errors ===
// stripe-js: https://github.com/stripe/stripe-js/issues/26
'Failed to load Stripe.js',
// [Joshen] This event started occurring after our fix in the org dropdown by reading the slug from
// the URL params instead of the store, but we cannot repro locally, staging nor on prod
// Safe to ignore since it's not a user-facing issue + we've not received any user feedback/report about it
// Ref: https://github.com/supabase/supabase/pull/9729
'The provided `href` (/org/[slug]/general) value is missing query values (slug)',
'The provided `href` (/org/[slug]/team) value is missing query values (slug)',
'The provided `href` (/org/[slug]/billing) value is missing query values (slug)',
'The provided `href` (/org/[slug]/invoices) value is missing query values (slug)',
// [Joshen] Seems to be from hcaptcha
// hCaptcha
"undefined is not an object (evaluating 'n.chat.setReady')",
"undefined is not an object (evaluating 'i.chat.setReady')",
// [Terry] When users paste in an embedded GitHub Gist
// Error thrown by `sql-formatter` lexer when given invalid input
// Original format: new Error(`Parse error: Unexpected "${text}" at line ${line} column ${col}`)

// === Next.js internals ===
// Ref: https://github.com/supabase/supabase/pull/9729
/The provided `href` \(\/org\/\[slug\]\/.*\) value is missing query values/,
// Next.js throws these during navigation, not actual errors
'NEXT_NOT_FOUND',
'NEXT_REDIRECT',

// === User input errors (not bugs) ===
// sql-formatter lexer on invalid SQL input
/^Parse error: Unexpected ".+" at line \d+ column \d+$/,
// [Joshen] IMO, should be caught on API if there's anything to handle - FE shouldn't dupe this alert

// === Network / infrastructure (not actionable on FE) ===
/504 Gateway Time-out/,
// [Joshen] This is the one caused by Google translate in the browser + 3rd party extensions
'Network request failed',
'Failed to fetch',
'Load failed',
'AbortError',
'TypeError: cancelled',
'TypeError: Cancelled',

// === Browser extensions & Google Translate DOM manipulation ===
'Node.insertBefore: Child to insert before is not a child of this node',
// [Ali] Google Translate / browser extension DOM manipulation errors
"NotFoundError: Failed to execute 'removeChild' on 'Node'",
"NotFoundError: Failed to execute 'insertBefore' on 'Node'",
'NotFoundError: The object can not be found here.',
// [Joshen] This one sprung up recently and I've no idea where this is coming from
'r.default.setDefaultLevel is not a function',
// [Joshen] Safe to ignore, it's an error from the copyToClipboard
'The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.',

// API/Network errors - should be handled by API layer, not actionable on frontend
/502/,
'upstream request timeout',
/upstream connect error or disconnect\/reset before headers/,
/Failed to fetch/,

// Google Translate / browser extension DOM manipulation errors
/Failed to execute 'removeChild' on 'Node'/,
"Cannot read properties of null (reading 'parentNode')",
"Cannot read properties of null (reading 'removeChild')",
"TypeError: can't access dead object",
/^NS_ERROR_/,

// Monaco editor errors - already filtering ResizeObserver, adding general monaco errors
/monaco-editor/,
// === Non-Error throws (extensions, third-party libs throwing strings/objects) ===
'Non-Error exception captured',
'Non-Error promise rejection captured',

// JWT/Auth errors - expected during session expiry, handled by auth flow
'jwt expired',
/InvalidJWTToken: Invalid value for JWT claim "exp"/,
// === Cross-origin script errors (no useful info) ===
'Script error.',
'Script error',

// User cancellation - intentional user action
'Canceled: Canceled',
// === React hydration mismatches caused by extensions modifying DOM ===
// Note: we only suppress the generic browser messages, NOT "Hydration failed because..."
// which can indicate real SSR/client mismatches in our own code.
/text content does not match/i,
/There was an error while hydrating/i,

// Third-party/extension errors
'html2canvas is not defined',
'[object Event]',
'ConnectorClass.onMessage(extensionPageScript)',
"Cannot destructure property 'address' of '(intermediate value)' as it is undefined.",

// Expected user flow errors
'Profile already exists: User already exists',
// === Web crawler / bot errors ===
'instantSearchSDKJSBridgeClearHighlight',

// JSON parse errors from invalid responses
'"undefined" is not valid JSON',
'SyntaxError: "undefined" is not valid JSON',
// === Misc known noise ===
'r.default.setDefaultLevel is not a function',
// Clipboard permission denied
'The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.',
// Facebook pixel
'fb_xd_fragment',
],
})

Expand Down
Loading
Loading