diff --git a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts
index c79f8ac313bc4..bd82fbd462199 100644
--- a/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts
+++ b/apps/docs/app/guides/getting-started/ai-prompts/[slug]/AiPrompts.utils.ts
@@ -6,9 +6,12 @@ import { readdir, readFile, stat } from 'node:fs/promises'
import { basename, extname, join } from 'node:path'
import { cache } from 'react'
import { visit, EXIT } from 'unist-util-visit'
+import { getCustomContent } from '~/lib/custom-content/getCustomContent'
import { EXAMPLES_DIRECTORY } from '~/lib/docs'
+const { metadataTitle } = getCustomContent(['metadata:title'])
+
const PROMPTS_DIRECTORY = join(EXAMPLES_DIRECTORY, 'prompts')
function parseMarkdown(markdown: string) {
@@ -85,7 +88,7 @@ export async function generateAiPromptMetadata(props: { params: Promise<{ slug:
if (!prompt) {
return {
- title: 'AI Prompt | Supabase Docs',
+ title: `AI Prompt | ${metadataTitle || 'Supabase'}`,
}
}
diff --git a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx
index d56363997a14c..9f15cac7d53fc 100644
--- a/apps/docs/app/guides/troubleshooting/[slug]/page.tsx
+++ b/apps/docs/app/guides/troubleshooting/[slug]/page.tsx
@@ -3,11 +3,12 @@ import { notFound } from 'next/navigation'
import TroubleshootingPage from '~/features/docs/Troubleshooting.page'
import { getAllTroubleshootingEntries, getArticleSlug } from '~/features/docs/Troubleshooting.utils'
import { PROD_URL } from '~/lib/constants'
+import { getCustomContent } from '~/lib/custom-content/getCustomContent'
-// 60 seconds/minute * 60 minutes/hour * 24 hours/day
-// export const revalidate = 86_400
export const dynamicParams = false
+const { metadataTitle } = getCustomContent(['metadata:title'])
+
export default async function TroubleshootingEntryPage(props: {
params: Promise<{ slug: string }>
}) {
@@ -34,7 +35,7 @@ export const generateMetadata = async (props: { params: Promise<{ slug: string }
const entry = allTroubleshootingEntries.find((entry) => getArticleSlug(entry) === slug)
return {
- title: 'Supabase Docs | Troubleshooting' + (entry ? ` | ${entry.data.title}` : ''),
+ title: `${metadataTitle || 'Supabase'} | Troubleshooting${entry ? ` | ${entry.data.title}` : ''}`,
alternates: {
canonical: `${PROD_URL}/guides/troubleshooting/${slug}`,
},
diff --git a/apps/docs/app/guides/troubleshooting/page.tsx b/apps/docs/app/guides/troubleshooting/page.tsx
index 1a519cf26b057..74582ef4838fd 100644
--- a/apps/docs/app/guides/troubleshooting/page.tsx
+++ b/apps/docs/app/guides/troubleshooting/page.tsx
@@ -15,9 +15,9 @@ import {
import { TROUBLESHOOTING_CONTAINER_ID } from '~/features/docs/Troubleshooting.utils.shared'
import { SidebarSkeleton } from '~/layouts/MainSkeleton'
import { PROD_URL } from '~/lib/constants'
+import { getCustomContent } from '~/lib/custom-content/getCustomContent'
-// 60 seconds/minute * 60 minutes/hour * 24 hours/day
-// export const revalidate = 86_400
+const { metadataTitle } = getCustomContent(['metadata:title'])
export default async function GlobalTroubleshootingPage() {
const troubleshootingEntries = await getAllTroubleshootingEntries()
@@ -60,7 +60,7 @@ export default async function GlobalTroubleshootingPage() {
}
export const metadata: Metadata = {
- title: 'Supabase Docs | Troubleshooting',
+ title: `${metadataTitle || 'Supabase'} | Troubleshooting`,
alternates: {
canonical: `${PROD_URL}/guides/troubleshooting`,
},
diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx
index 420f18093ce36..353f4a60b74bd 100644
--- a/apps/docs/app/layout.tsx
+++ b/apps/docs/app/layout.tsx
@@ -4,18 +4,23 @@ import '../styles/main.scss'
import '../styles/new-docs.scss'
import '../styles/prism-okaidia.scss'
-import { type Metadata, type Viewport } from 'next'
+import { TelemetryTagManager } from 'common'
import { genFaviconData } from 'common/MetaFavicons/app-router'
-
+import type { Metadata, Viewport } from 'next'
import { GlobalProviders } from '~/features/app.providers'
import { TopNavSkeleton } from '~/layouts/MainSkeleton'
import { BASE_PATH, IS_PRODUCTION } from '~/lib/constants'
-import { TelemetryTagManager } from 'common'
+import { getCustomContent } from '~/lib/custom-content/getCustomContent'
+
+const { metadataApplicationName, metadataTitle } = getCustomContent([
+ 'metadata:application_name',
+ 'metadata:title',
+])
const metadata: Metadata = {
- applicationName: 'Supabase Docs',
- title: 'Supabase Docs',
+ applicationName: metadataApplicationName,
+ title: metadataTitle,
description:
'Supabase is the Postgres development platform providing all the backend features you need to build a product.',
metadataBase: new URL('https://supabase.com'),
diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx
index 5e35276d15308..597634f91a37b 100644
--- a/apps/docs/components/HomePageCover.tsx
+++ b/apps/docs/components/HomePageCover.tsx
@@ -7,7 +7,7 @@ import Link from 'next/link'
import { isFeatureEnabled, useBreakpoint } from 'common'
import { cn, IconBackground } from 'ui'
import { IconPanel } from 'ui-patterns/IconPanel'
-import { useCustomContent } from '../hooks/custom-content/useCustomContent'
+import { getCustomContent } from '../lib/custom-content/getCustomContent'
import DocsCoverLogo from './DocsCoverLogo'
const { sdkDart: sdkDartEnabled, sdkKotlin: sdkKotlinEnabled } = isFeatureEnabled([
@@ -37,7 +37,7 @@ function AiPrompt({ className }: { className?: string }) {
const HomePageCover = (props) => {
const isXs = useBreakpoint(639)
const iconSize = isXs ? 'sm' : 'lg'
- const { homepageHeading } = useCustomContent(['homepage:heading'])
+ const { homepageHeading } = getCustomContent(['homepage:heading'])
const frameworks = [
{
diff --git a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx
index 1ae88b582aea1..0a4600478c2ef 100644
--- a/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx
+++ b/apps/docs/components/Navigation/NavigationMenu/TopNavBar.tsx
@@ -9,7 +9,7 @@ import { memo, useState } from 'react'
import { useIsLoggedIn, useIsUserLoading, useUser } from 'common'
import { Button, buttonVariants, cn } from 'ui'
import { AuthenticatedDropdownMenu, CommandMenuTrigger } from 'ui-patterns'
-import { useCustomContent } from '../../../hooks/custom-content/useCustomContent'
+import { getCustomContent } from '../../../lib/custom-content/getCustomContent'
import GlobalNavigationMenu from './GlobalNavigationMenu'
import useDropdownMenu from './useDropdownMenu'
@@ -110,7 +110,7 @@ const TopNavBar: FC = () => {
}
const HeaderLogo = memo(() => {
- const { navigationLogo } = useCustomContent(['navigation:logo'])
+ const { navigationLogo } = getCustomContent(['navigation:logo'])
return (
0
? {
diff --git a/apps/docs/features/docs/TroubleshootingSection.page.tsx b/apps/docs/features/docs/TroubleshootingSection.page.tsx
index 9358ca24b19ba..9fdeb8b61738d 100644
--- a/apps/docs/features/docs/TroubleshootingSection.page.tsx
+++ b/apps/docs/features/docs/TroubleshootingSection.page.tsx
@@ -12,6 +12,9 @@ import {
getTroubleshootingKeywordsByTopic,
} from '~/features/docs/Troubleshooting.utils'
import { PROD_URL } from '~/lib/constants'
+import { getCustomContent } from '~/lib/custom-content/getCustomContent'
+
+const { metadataTitle } = getCustomContent(['metadata:title'])
interface SectionTroubleshootingPageProps {
topic: ITroubleshootingMetadata['topics'][number]
@@ -60,7 +63,7 @@ export function generateSectionTroubleshootingMetadata(
sectionName: string
): Metadata {
return {
- title: `Supabase Docs | ${sectionName} Troubleshooting`,
+ title: `${metadataTitle ?? 'Supabase'} | ${sectionName} Troubleshooting`,
alternates: {
canonical: `${PROD_URL}/guides/${topic}/troubleshooting`,
},
diff --git a/apps/docs/hooks/custom-content/CustomContent.types.ts b/apps/docs/lib/custom-content/CustomContent.types.ts
similarity index 73%
rename from apps/docs/hooks/custom-content/CustomContent.types.ts
rename to apps/docs/lib/custom-content/CustomContent.types.ts
index ce845cef5b59d..1554064ed772d 100644
--- a/apps/docs/hooks/custom-content/CustomContent.types.ts
+++ b/apps/docs/lib/custom-content/CustomContent.types.ts
@@ -1,5 +1,7 @@
export type CustomContentTypes = {
homepageHeading: string
+ metadataApplicationName: string
+ metadataTitle: string
navigationLogo: {
light: string
dark: string
diff --git a/apps/docs/hooks/custom-content/custom-content.json b/apps/docs/lib/custom-content/custom-content.json
similarity index 65%
rename from apps/docs/hooks/custom-content/custom-content.json
rename to apps/docs/lib/custom-content/custom-content.json
index 71c311548424a..54504e901179e 100644
--- a/apps/docs/hooks/custom-content/custom-content.json
+++ b/apps/docs/lib/custom-content/custom-content.json
@@ -1,5 +1,7 @@
{
"homepage:heading": "Supabase Documentation",
+ "metadata:application_name": "Supabase Docs",
+ "metadata:title": "Supabase Docs",
"navigation:logo": {
"light": "/docs/supabase-light.svg",
"dark": "/docs/supabase-dark.svg"
diff --git a/apps/docs/hooks/custom-content/useCustomContent.test.ts b/apps/docs/lib/custom-content/getCustomContent.test.ts
similarity index 57%
rename from apps/docs/hooks/custom-content/useCustomContent.test.ts
rename to apps/docs/lib/custom-content/getCustomContent.test.ts
index 38145d93f5f63..c48f5413e623e 100644
--- a/apps/docs/hooks/custom-content/useCustomContent.test.ts
+++ b/apps/docs/lib/custom-content/getCustomContent.test.ts
@@ -1,17 +1,11 @@
-/*
- * @vitest-environment jsdom
- */
-
-import { cleanup, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
beforeEach(() => {
vi.clearAllMocks()
vi.resetModules()
- cleanup()
})
-describe('useCustomContent', () => {
+describe('getCustomContent', () => {
it('should return null if content is not found in the custom-content.json file', async () => {
vi.doMock('./custom-content.json', () => ({
default: {
@@ -19,9 +13,9 @@ describe('useCustomContent', () => {
},
}))
- const { useCustomContent } = await import('./useCustomContent')
- const { result } = renderHook(() => useCustomContent(['navigation:logo']))
- expect(result.current.navigationLogo).toEqual(null)
+ const { getCustomContent } = await import('./getCustomContent')
+ const result = getCustomContent(['navigation:logo'])
+ expect(result.navigationLogo).toEqual(null)
})
it('should return the content for the key passed in if it exists in the custom-content.json file', async () => {
@@ -35,12 +29,12 @@ describe('useCustomContent', () => {
},
}))
- const { useCustomContent } = await import('./useCustomContent')
- const { result } = renderHook(() => useCustomContent(['navigation:logo', 'homepage:heading']))
- expect(result.current.navigationLogo).toEqual({
+ const { getCustomContent } = await import('./getCustomContent')
+ const result = getCustomContent(['navigation:logo', 'homepage:heading'])
+ expect(result.navigationLogo).toEqual({
light: 'https://example.com/logo-light.svg',
dark: 'https://example.com/logo-dark.svg',
})
- expect(result.current.homepageHeading).toEqual('Custom Heading')
+ expect(result.homepageHeading).toEqual('Custom Heading')
})
})
diff --git a/apps/docs/hooks/custom-content/useCustomContent.ts b/apps/docs/lib/custom-content/getCustomContent.ts
similarity index 60%
rename from apps/docs/hooks/custom-content/useCustomContent.ts
rename to apps/docs/lib/custom-content/getCustomContent.ts
index d88a73339a1a4..07e327f4fbf40 100644
--- a/apps/docs/hooks/custom-content/useCustomContent.ts
+++ b/apps/docs/lib/custom-content/getCustomContent.ts
@@ -1,17 +1,8 @@
-/*
- * [Charis 2025-09-29] This file is a duplicate of studio's useCustomContent.ts
- * for now.
- *
- * We should probably consolidate these two files in the future, but we want to
- * get this change shipped quickly without worrying about smoke testing all the
- * components affected by a refactor.
- */
-
import type { CustomContentTypes } from './CustomContent.types'
import customContentRaw from './custom-content.json'
const customContentStaticObj = customContentRaw as Omit
-type CustomContent = keyof typeof customContentStaticObj
+export type CustomContent = keyof typeof customContentStaticObj
type SnakeToCamelCase = S extends `${infer First}_${infer Rest}`
? `${First}${SnakeToCamelCase>}`
@@ -29,21 +20,16 @@ function contentToCamelCase(feature: CustomContent) {
.join('') as CustomContentToCamelCase
}
-const useCustomContent = (
- contents: T
-): {
+type CustomContentResult = {
[key in CustomContentToCamelCase]:
| CustomContentTypes[CustomContentToCamelCase]
| null
-} => {
- // [Joshen] Running into some TS errors without the `as` here - must be overlooking something super simple
+}
+
+export const getCustomContent = (
+ contents: T
+): CustomContentResult => {
return Object.fromEntries(
contents.map((content) => [contentToCamelCase(content), customContentStaticObj[content]])
- ) as {
- [key in CustomContentToCamelCase]:
- | CustomContentTypes[CustomContentToCamelCase]
- | null
- }
+ ) as CustomContentResult
}
-
-export { useCustomContent }
diff --git a/apps/docs/scripts/llms.ts b/apps/docs/scripts/llms.ts
index 9d68e431ed636..95ad95d57ef8a 100644
--- a/apps/docs/scripts/llms.ts
+++ b/apps/docs/scripts/llms.ts
@@ -4,7 +4,8 @@ import 'dotenv/config'
import fs from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
-import { isFeatureEnabled } from 'common/enabled-features'
+import { isFeatureEnabled } from '../../../packages/common/enabled-features/index.js'
+import { getCustomContent } from '../lib/custom-content/getCustomContent.js'
import {
fetchCliLibReferenceSource,
fetchCSharpLibReferenceSource,
@@ -35,6 +36,8 @@ const {
sdkSwift: sdkSwiftEnabled,
} = isFeatureEnabled(['sdk:csharp', 'sdk:dart', 'sdk:kotlin', 'sdk:python', 'sdk:swift'])
+const { metadataTitle } = getCustomContent(['metadata:title'])
+
function toLink(source: Source) {
return `[${source.title}](https://supabase.com/${source.relPath})`
}
@@ -115,7 +118,7 @@ async function generateMainLlmsTxt() {
const sourceLinks = SOURCES.filter((source) => source.enabled !== false)
.map((source) => `- ${toLink(source)}`)
.join('\n')
- const fullText = `# Supabase Docs\n\n${sourceLinks}`
+ const fullText = `# ${metadataTitle}\n\n${sourceLinks}`
fs.writeFile('public/llms.txt', fullText)
}
diff --git a/apps/studio/components/interfaces/HomePageActions.tsx b/apps/studio/components/interfaces/HomePageActions.tsx
index 2bfdfcb673b29..cfaacaf23d6a0 100644
--- a/apps/studio/components/interfaces/HomePageActions.tsx
+++ b/apps/studio/components/interfaces/HomePageActions.tsx
@@ -91,7 +91,7 @@ export const HomePageActions = ({
{ key: PROJECT_STATUS.ACTIVE_HEALTHY, label: 'Active' },
{ key: PROJECT_STATUS.INACTIVE, label: 'Paused' },
].map(({ key, label }) => (
-
+
{
- const encoder = new TextEncoder()
- const data = encoder.encode(id)
- const hashBuffer = await crypto.subtle.digest('SHA-256', data)
- const hashArray = Array.from(new Uint8Array(hashBuffer))
- const base64String = btoa(hashArray.map((byte) => String.fromCharCode(byte)).join(''))
-
- return base64String
-}
-
-const GroupsTelemetry = ({ hasAcceptedConsent }: { hasAcceptedConsent: boolean }) => {
- // Although this is "technically" breaking the rules of hooks
- // IS_PLATFORM never changes within a session, so this won't cause any issues
- if (!IS_PLATFORM) return null
-
- const user = useUser()
- const router = useRouter()
- const { ref, slug } = useParams()
- const { data: organization } = useSelectedOrganizationQuery()
-
- const previousPathname = usePrevious(router.pathname)
-
- const { mutate: sendGroupsIdentify } = useSendGroupsIdentifyMutation()
- const { mutate: sendGroupsReset } = useSendGroupsResetMutation()
-
- const title = typeof document !== 'undefined' ? document?.title : ''
- const referrer = typeof document !== 'undefined' ? document?.referrer : ''
- useTelemetryCookie({ hasAcceptedConsent, title, referrer })
-
- useEffect(() => {
- // don't set the sentry user id if the user hasn't logged in (so that Sentry errors show null user id instead of anonymous id)
- if (!user?.id) {
- return
- }
-
- const setSentryId = async () => {
- let sentryUserId = localStorage.getItem(LOCAL_STORAGE_KEYS.SENTRY_USER_ID)
- if (!sentryUserId) {
- sentryUserId = await getAnonId(user?.id)
- localStorage.setItem(LOCAL_STORAGE_KEYS.SENTRY_USER_ID, sentryUserId)
- }
- Sentry.setUser({ id: sentryUserId })
- }
-
- // if an error happens, continue without setting a sentry id
- setSentryId().catch((e) => console.error(e))
- }, [user?.id])
-
- useEffect(() => {
- const isLandingOnProjectRoute =
- router.pathname.includes('[ref]') && previousPathname === router.pathname
- const isEnteringProjectRoute =
- !(previousPathname ?? '').includes('[ref]') && router.pathname.includes('[ref]')
- const isLeavingProjectRoute =
- (previousPathname ?? '').includes('[ref]') && !router.pathname.includes('[ref]')
-
- const isLandingOnOrgRoute =
- router.pathname.includes('[slug]') && previousPathname === router.pathname
- const isEnteringOrgRoute =
- !(previousPathname ?? '').includes('[slug]') && router.pathname.includes('[slug]')
- const isLeavingOrgRoute =
- (previousPathname ?? '').includes('[slug]') && !router.pathname.includes('[slug]')
-
- if (hasAcceptedConsent) {
- if (ref && (isLandingOnProjectRoute || isEnteringProjectRoute)) {
- sendGroupsIdentify({ organization_slug: organization?.slug, project_ref: ref as string })
- } else if (slug && (isLandingOnOrgRoute || isEnteringOrgRoute)) {
- sendGroupsIdentify({ organization_slug: slug, project_ref: undefined })
- } else if (isLeavingProjectRoute || isLeavingOrgRoute) {
- sendGroupsReset({
- reset_organization: isLeavingOrgRoute || isLeavingProjectRoute,
- reset_project: isLeavingProjectRoute,
- })
- }
- }
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [hasAcceptedConsent, slug, ref, router.pathname])
-
- return null
-}
-
-export default GroupsTelemetry
diff --git a/apps/studio/data/permissions/permissions-query.ts b/apps/studio/data/permissions/permissions-query.ts
index 783ed966bd37d..5de599ed44bd8 100644
--- a/apps/studio/data/permissions/permissions-query.ts
+++ b/apps/studio/data/permissions/permissions-query.ts
@@ -11,18 +11,10 @@ export type PermissionsResponse = Permission[]
export async function getPermissions(signal?: AbortSignal) {
const { data, error } = await get('/platform/profile/permissions', { signal })
if (error) {
- const statusCode = (!!error && typeof error === 'object' && (error as any).code) || 'unknown'
-
- // This is to avoid sending 4XX errors
- // But we still want to capture errors without a status code or 5XXs
- // since those may require investigation if they spike
- const sendError = statusCode >= 500 || statusCode === 'unknown'
handleError(error, {
- alwaysCapture: sendError,
sentryContext: {
tags: {
permissionsQuery: true,
- statusCode,
},
contexts: {
rawError: error,
diff --git a/apps/studio/data/telemetry/send-groups-identify-mutation.ts b/apps/studio/data/telemetry/send-groups-identify-mutation.ts
deleted file mode 100644
index 07d8b316fc9dc..0000000000000
--- a/apps/studio/data/telemetry/send-groups-identify-mutation.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useMutation, UseMutationOptions } from '@tanstack/react-query'
-
-import { components } from 'api-types'
-import { hasConsented } from 'common'
-import { handleError, post } from 'data/fetchers'
-import { IS_PLATFORM } from 'lib/constants'
-import type { ResponseError } from 'types'
-
-export type SendGroupsIdentifyVariables = components['schemas']['TelemetryGroupsIdentityBody']
-
-export async function sendGroupsIdentify({ body }: { body: SendGroupsIdentifyVariables }) {
- const consent = hasConsented()
-
- if (!consent || !IS_PLATFORM) return undefined
-
- const { data, error } = await post(`/platform/telemetry/groups/identify`, {
- body,
- credentials: 'include',
- })
- if (error) handleError(error)
- return data
-}
-
-type SendGroupsIdentifyData = Awaited>
-
-export const useSendGroupsIdentifyMutation = ({
- onSuccess,
- onError,
- ...options
-}: Omit<
- UseMutationOptions,
- 'mutationFn'
-> = {}) => {
- return useMutation(
- (vars) => sendGroupsIdentify({ body: vars }),
- {
- async onSuccess(data, variables, context) {
- await onSuccess?.(data, variables, context)
- },
- async onError(data, variables, context) {
- if (onError === undefined) {
- console.error(`Failed to send Telemetry groups identify: ${data.message}`)
- } else {
- onError(data, variables, context)
- }
- },
- ...options,
- }
- )
-}
diff --git a/apps/studio/data/telemetry/send-groups-reset-mutation.ts b/apps/studio/data/telemetry/send-groups-reset-mutation.ts
deleted file mode 100644
index 6f8524547f480..0000000000000
--- a/apps/studio/data/telemetry/send-groups-reset-mutation.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useMutation, UseMutationOptions } from '@tanstack/react-query'
-
-import { components } from 'api-types'
-import { hasConsented } from 'common'
-import { handleError, post } from 'data/fetchers'
-import { IS_PLATFORM } from 'lib/constants'
-import type { ResponseError } from 'types'
-
-export type SendGroupsResetVariables = components['schemas']['TelemetryGroupsResetBody']
-
-export async function sendGroupsReset({ body }: { body: SendGroupsResetVariables }) {
- const consent = hasConsented()
-
- if (!consent || !IS_PLATFORM) return undefined
-
- const { data, error } = await post(`/platform/telemetry/groups/reset`, {
- body,
- credentials: 'include',
- })
- if (error) handleError(error)
- return data
-}
-
-type SendGroupsResetData = Awaited>
-
-export const useSendGroupsResetMutation = ({
- onSuccess,
- onError,
- ...options
-}: Omit<
- UseMutationOptions,
- 'mutationFn'
-> = {}) => {
- return useMutation(
- (vars) => sendGroupsReset({ body: vars }),
- {
- async onSuccess(data, variables, context) {
- await onSuccess?.(data, variables, context)
- },
- async onError(data, variables, context) {
- if (onError === undefined) {
- console.error(`Failed to send Telemetry groups reset: ${data.message}`)
- } else {
- onError(data, variables, context)
- }
- },
- ...options,
- }
- )
-}
diff --git a/apps/studio/lib/telemetry.tsx b/apps/studio/lib/telemetry.tsx
index 5c4f9932aef96..4e17c8cf78d77 100644
--- a/apps/studio/lib/telemetry.tsx
+++ b/apps/studio/lib/telemetry.tsx
@@ -1,7 +1,20 @@
-import { PageTelemetry } from 'common'
+import * as Sentry from '@sentry/nextjs'
+import { useEffect } from 'react'
+
+import { LOCAL_STORAGE_KEYS, PageTelemetry, useUser } from 'common'
+import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { useConsentToast } from 'ui-patterns/consent'
-import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
+
+const getAnonId = async (id: string) => {
+ const encoder = new TextEncoder()
+ const data = encoder.encode(id)
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data)
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
+ const base64String = btoa(hashArray.map((byte) => String.fromCharCode(byte)).join(''))
+
+ return base64String
+}
export function Telemetry() {
// Although this is "technically" breaking the rules of hooks
@@ -13,6 +26,27 @@ export function Telemetry() {
// always available in the URL params
const { data: organization } = useSelectedOrganizationQuery()
+ const user = useUser()
+
+ useEffect(() => {
+ // don't set the sentry user id if the user hasn't logged in (so that Sentry errors show null user id instead of anonymous id)
+ if (!user?.id) {
+ return
+ }
+
+ const setSentryId = async () => {
+ let sentryUserId = localStorage.getItem(LOCAL_STORAGE_KEYS.SENTRY_USER_ID)
+ if (!sentryUserId) {
+ sentryUserId = await getAnonId(user?.id)
+ localStorage.setItem(LOCAL_STORAGE_KEYS.SENTRY_USER_ID, sentryUserId)
+ }
+ Sentry.setUser({ id: sentryUserId })
+ }
+
+ // if an error happens, continue without setting a sentry id
+ setSentryId().catch((e) => console.error(e))
+ }, [user?.id])
+
return (
{
}
)
- const regionError = smartRegionEnabled ? availableRegionsError : defaultRegionError
- const defaultRegion = smartRegionEnabled
- ? availableRegionsData?.recommendations.smartGroup.name
- : defaultProvider === 'AWS_NIMBUS'
+ const regionError =
+ smartRegionEnabled && defaultProvider !== 'AWS_NIMBUS'
+ ? availableRegionsError
+ : defaultRegionError
+ const defaultRegion =
+ defaultProvider === 'AWS_NIMBUS'
? AWS_REGIONS.EAST_US.displayName
- : _defaultRegion
+ : smartRegionEnabled
+ ? availableRegionsData?.recommendations.smartGroup.name
+ : _defaultRegion
const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects')
diff --git a/packages/pg-meta/src/sql/tables.ts b/packages/pg-meta/src/sql/tables.ts
index 3b08a0ae71c21..379663cb8edeb 100644
--- a/packages/pg-meta/src/sql/tables.ts
+++ b/packages/pg-meta/src/sql/tables.ts
@@ -28,27 +28,24 @@ FROM
JOIN pg_class c ON nc.oid = c.relnamespace
left join (
select
- table_id,
- jsonb_agg(_pk.*) as primary_keys
- from (
- select
- n.nspname as schema,
- c.relname as table_name,
- a.attname as name,
- c.oid :: int8 as table_id
- from
- pg_index i,
- pg_class c,
- pg_attribute a,
- pg_namespace n
- where
- i.indrelid = c.oid
- and c.relnamespace = n.oid
- and a.attrelid = c.oid
- and a.attnum = any (i.indkey)
- and i.indisprimary
- ) as _pk
- group by table_id
+ c.oid::int8 as table_id,
+ jsonb_agg(
+ jsonb_build_object(
+ 'table_id', c.oid::int8,
+ 'schema', n.nspname,
+ 'table_name', c.relname,
+ 'name', a.attname
+ )
+ order by array_position(i.indkey, a.attnum)
+ ) as primary_keys
+ from
+ pg_index i
+ join pg_class c on i.indrelid = c.oid
+ join pg_namespace n on c.relnamespace = n.oid
+ join pg_attribute a on a.attrelid = c.oid and a.attnum = any(i.indkey)
+ where
+ i.indisprimary
+ group by c.oid
) as pk
on pk.table_id = c.oid
left join (