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
15 changes: 15 additions & 0 deletions apps/docs/content/guides/telemetry/log-drains.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The following table lists the supported destinations and the required setup conf
| Generic HTTP endpoint | HTTP | URL <br /> HTTP Version <br/> Gzip <br /> Headers |
| DataDog | HTTP | API Key <br /> Region |
| Loki | HTTP | URL <br /> Headers |
| Sentry | HTTP | DSN |

HTTP requests are batched with a max of 250 logs or 1 second intervals, whichever happens first. Logs are compressed via Gzip if the destination supports it.

Expand Down Expand Up @@ -196,6 +197,20 @@ The `event_message` and `timestamp` fields will be dropped from the events to av

Loki must be configured to accept **structured metadata**, and it is advised to increase the default maximum number of structured metadata fields to at least 500 to accommodate large log event payloads of different products.

## Sentry

Logs are sent to Sentry as part of [Sentry's Logging Product](https://docs.sentry.io/product/explore/logs/). Ingesting Supabase logs as Sentry errors is currently not supported.

To setup the Sentry log drain, you need to do the following:

1. Grab your DSN from your [Sentry project settings](https://docs.sentry.io/concepts/key-terms/dsn-explainer/). It should be of the format `{PROTOCOL}://{PUBLIC_KEY}:{SECRET_KEY}@{HOST}{PATH}/{PROJECT_ID}`.
2. Create log drain in [Supabase dashboard](/dashboard/project/_/settings/log-drains)
3. Watch for events in the [Sentry Logs page](https://sentry.io/explore/logs/)

All fields from the log event are attached as attributes to the Sentry log, which can be used for filtering and grouping in the Sentry UI. There are no limits to cardinality or the number of attributes that can be attached to a log.

If you are self-hosting Sentry, Sentry Logs are only supported in self-hosted version [25.9.0](https://github.com/getsentry/self-hosted/releases/tag/25.9.0) and later.

## Pricing

For a detailed breakdown of how charges are calculated, refer to [Manage Log Drain usage](/docs/guides/platform/manage-your-usage/log-drains).
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { debounce } from 'lodash'
import { useRef, useState } from 'react'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'
Expand All @@ -10,7 +9,7 @@ import { useProjectCloneMutation } from 'data/projects/clone-mutation'
import { useCloneBackupsQuery } from 'data/projects/clone-query'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { passwordStrength } from 'lib/helpers'
import { passwordStrength } from 'lib/password-strength'
import { generateStrongPassword } from 'lib/project'
import {
Button,
Expand Down Expand Up @@ -86,10 +85,6 @@ export const CreateNewProjectDialog = ({
},
})

const delayedCheckPasswordStrength = useRef(
debounce((value: string) => checkPasswordStrength(value), 300)
).current

async function checkPasswordStrength(value: string) {
const { message, strength } = await passwordStrength(value)
setPasswordStrengthScore(strength)
Expand All @@ -99,7 +94,7 @@ export const CreateNewProjectDialog = ({
const generatePassword = () => {
const password = generateStrongPassword()
form.setValue('password', password)
delayedCheckPasswordStrength(password)
checkPasswordStrength(password)
}

return (
Expand Down Expand Up @@ -173,7 +168,7 @@ export const CreateNewProjectDialog = ({
if (value == '') {
setPasswordStrengthScore(-1)
setPasswordStrengthMessage('')
} else delayedCheckPasswordStrength(value)
} else checkPasswordStrength(value)
}}
descriptionText={
<PasswordStrengthBar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { z } from 'zod'

import { useParams } from 'common'
import { useFlag, useParams } from 'common'
import { DocsButton } from 'components/ui/DocsButton'
import { LogDrainData, useLogDrainsQuery } from 'data/log-drains/log-drains-query'
import { DOCS_URL } from 'lib/constants'
Expand Down Expand Up @@ -80,6 +80,7 @@ const formUnion = z.discriminatedUnion('type', [
}),
z.object({
type: z.literal('sentry'),
dsn: z.string().min(1, { message: 'Sentry DSN is required' }),
}),
])

Expand Down Expand Up @@ -156,6 +157,8 @@ export function LogDrainDestinationSheetForm({
}
const DEFAULT_HEADERS = mode === 'create' ? CREATE_DEFAULT_HEADERS : defaultConfig?.headers || {}

const sentryEnabled = useFlag('SentryLogDrain')

const { ref } = useParams()
const { data: logDrains } = useLogDrainsQuery({
ref,
Expand All @@ -164,6 +167,11 @@ export function LogDrainDestinationSheetForm({
const defaultType = defaultValues?.type || 'webhook'
const [newCustomHeader, setNewCustomHeader] = useState({ name: '', value: '' })

const baseValues = {
name: defaultValues?.name || '',
description: defaultValues?.description || '',
}

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
values: {
Expand All @@ -178,6 +186,7 @@ export function LogDrainDestinationSheetForm({
region: defaultConfig?.region || '',
username: defaultConfig?.username || '',
password: defaultConfig?.password || '',
dsn: defaultConfig?.dsn || '',
},
})

Expand Down Expand Up @@ -274,7 +283,7 @@ export function LogDrainDestinationSheetForm({
/>
<LogDrainFormItem
value="description"
placeholder="My Destination"
placeholder="Optional description"
label="Description"
formControl={form.control}
/>
Expand All @@ -293,16 +302,18 @@ export function LogDrainDestinationSheetForm({
{LOG_DRAIN_TYPES.find((t) => t.value === type)?.name}
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{LOG_DRAIN_TYPES.map((type) => (
<SelectItem_Shadcn_
value={type.value}
key={type.value}
id={type.value}
className="text-left"
>
{type.name}
</SelectItem_Shadcn_>
))}
{LOG_DRAIN_TYPES.filter((t) => t.value !== 'sentry' || sentryEnabled).map(
(type) => (
<SelectItem_Shadcn_
value={type.value}
key={type.value}
id={type.value}
className="text-left"
>
{type.name}
</SelectItem_Shadcn_>
)
)}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
Expand Down Expand Up @@ -456,6 +467,31 @@ export function LogDrainDestinationSheetForm({
/>
</div>
)}
{type === 'sentry' && (
<div className="grid gap-4 px-content">
<LogDrainFormItem
type="text"
value="dsn"
label="DSN"
placeholder="https://<project_id>@o<organization_id>.ingest.sentry.io/<project_id>"
formControl={form.control}
description={
<>
The DSN obtained from the Sentry dashboard. Read more about DSNs{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="text-sm underline transition hover:text-foreground"
href="https://docs.sentry.io/concepts/key-terms/dsn-explainer/"
>
here
</a>
.
</>
}
/>
</div>
)}
<FormMessage_Shadcn_ />
</div>
</form>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Datadog, Grafana, Sentry } from 'icons'
import { components } from 'api-types'
import { Datadog, Grafana } from 'icons'
import { BracesIcon } from 'lucide-react'

const iconProps = {
Expand Down Expand Up @@ -28,6 +28,13 @@ export const LOG_DRAIN_TYPES = [
'Loki is an open-source log aggregation system designed to store and query logs from multiple sources',
icon: <Grafana {...iconProps} fill="currentColor" strokeWidth={0} />,
},
{
value: 'sentry',
name: 'Sentry',
description:
'Sentry is an application monitoring service that helps developers identify and debug performance issues and errors',
icon: <Sentry {...iconProps} fill="currentColor" strokeWidth={0} />,
},
] as const

export const LOG_DRAIN_SOURCE_VALUES = LOG_DRAIN_TYPES.map((source) => source.value)
Expand Down
6 changes: 4 additions & 2 deletions apps/studio/components/interfaces/LogDrains/LogDrains.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'

import { useParams } from 'common'
import { useFlag, useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import CardButton from 'components/ui/CardButton'
import Panel from 'components/ui/Panel'
Expand Down Expand Up @@ -55,6 +55,8 @@ export function LogDrains({
enabled: logDrainsEnabled,
}
)
const sentryEnabled = useFlag('SentryLogDrain')

const { mutate: deleteLogDrain } = useDeleteLogDrainMutation({
onSuccess: () => {
setIsDeleteModalOpen(false)
Expand Down Expand Up @@ -91,7 +93,7 @@ export function LogDrains({
if (!isLoading && logDrains?.length === 0) {
return (
<div className="grid lg:grid-cols-2 gap-3">
{LOG_DRAIN_TYPES.map((src) => (
{LOG_DRAIN_TYPES.filter((t) => t.value !== 'sentry' || sentryEnabled).map((src) => (
<CardButton
key={src.value}
title={src.name}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { debounce } from 'lodash'
import { useRef } from 'react'
import { UseFormReturn } from 'react-hook-form'

import Panel from 'components/ui/Panel'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
import passwordStrength from 'lib/password-strength'
import { passwordStrength } from 'lib/password-strength'
import { generateStrongPassword } from 'lib/project'
import { FormControl_Shadcn_, FormField_Shadcn_ } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
Expand Down Expand Up @@ -37,15 +35,11 @@ export const DatabasePasswordInput = ({
setPasswordStrengthMessage(message)
}

const delayedCheckPasswordStrength = useRef(
debounce((value) => checkPasswordStrength(value), 300)
).current

// [Refactor] DB Password could be a common component used in multiple pages with repeated logic
function generatePassword() {
const password = generateStrongPassword()
form.setValue('dbPass', password)
delayedCheckPasswordStrength(password)
checkPasswordStrength(password)
}

return (
Expand Down Expand Up @@ -88,7 +82,7 @@ export const DatabasePasswordInput = ({
await form.setValue('dbPassStrength', 0)
await form.trigger('dbPass')
} else {
await delayedCheckPasswordStrength(value)
await checkPasswordStrength(value)
}
}}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { debounce } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'

import { useParams } from 'common'
Expand All @@ -12,7 +11,7 @@ import { useDatabasePasswordResetMutation } from 'data/database/database-passwor
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants'
import passwordStrength from 'lib/password-strength'
import { passwordStrength } from 'lib/password-strength'
import { generateStrongPassword } from 'lib/project'
import { Button, Input, Modal } from 'ui'

Expand Down Expand Up @@ -62,17 +61,13 @@ const ResetDbPassword = ({ disabled = false }) => {
setPasswordStrengthMessage(message)
}

const delayedCheckPasswordStrength = useRef(
debounce((value) => checkPasswordStrength(value), 300)
).current

const onDbPassChange = (e: any) => {
const value = e.target.value
setPassword(value)
if (value == '') {
setPasswordStrengthScore(-1)
setPasswordStrengthMessage('')
} else delayedCheckPasswordStrength(value)
} else checkPasswordStrength(value)
}

const confirmResetDbPass = async () => {
Expand All @@ -86,7 +81,7 @@ const ResetDbPassword = ({ disabled = false }) => {
function generatePassword() {
const password = generateStrongPassword()
setPassword(password)
delayedCheckPasswordStrength(password)
checkPasswordStrength(password)
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export function useDatabaseGotoCommands(options?: CommandOptions) {
id: 'nav-database-hooks',
name: 'Webhooks',
value: 'Database: Webhooks',
route: `/project/${ref}/integrations/hooks`,
route: `/project/${ref}/integrations/webhooks`,
defaultHidden: true,
},
{
Expand Down
5 changes: 3 additions & 2 deletions apps/studio/components/ui/PasswordStrengthBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ const PasswordStrengthBar = ({
</div>
)}
<p>
{passwordStrengthMessage
{(passwordStrengthMessage
? passwordStrengthMessage
: 'This is the password to your Postgres database, so it must be strong and hard to guess.'}{' '}
: 'This is the password to your Postgres database, so it must be strong and hard to guess.') +
' '}
<span
className="text-inherit underline hover:text-foreground transition-colors cursor-pointer"
onClick={generateStrongPassword}
Expand Down
9 changes: 8 additions & 1 deletion apps/studio/data/query-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,17 @@ export function getQueryClient() {
return false
}

// Skip retries for specific pathnames to avoid unnecessary load
// CRITICAL: We must still retry 429 (rate limit) errors even on these pathnames.
// Without this exception, queries fail immediately on rate limits, causing the
// frontend to issue fresh requests (via refetch/user actions), which amplifies
// the rate limiting problem. By retrying 429s with proper backoff (using the
// retryAfter header below), we respect rate limits and prevent request storms.
if (
error instanceof ResponseError &&
error.requestPathname &&
SKIP_RETRY_PATHNAME_MATCHERS.some((matchFn) => matchFn(error.requestPathname!))
SKIP_RETRY_PATHNAME_MATCHERS.some((matchFn) => matchFn(error.requestPathname!)) &&
error.code !== 429
) {
return false
}
Expand Down
1 change: 0 additions & 1 deletion apps/studio/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { default as passwordStrength } from './password-strength'
export { default as uuidv4 } from './uuid'
import { UIEvent } from 'react'
import type { TablesData } from '../data/tables/tables-query'
Expand Down
Loading
Loading