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 (