diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 8bc19e260d2..de6979679e8 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -1,11 +1,9 @@ import { ExternalLink, Eye, EyeOff, FlaskConical } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/router' import { useEffect, useState } from 'react' -import { useTelemetryProps } from 'common' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { LOCAL_STORAGE_KEYS } from 'lib/constants' -import Telemetry from 'lib/telemetry' import { useAppStateSnapshot } from 'state/app-state' import { Button, Modal, ScrollArea, cn } from 'ui' import APISidePanelPreview from './APISidePanelPreview' @@ -29,10 +27,9 @@ const FeaturePreviewModal = () => { }, ] - const router = useRouter() const snap = useAppStateSnapshot() - const telemetryProps = useTelemetryProps() const featurePreviewContext = useFeaturePreviewContext() + const { mutate: sendEvent } = useSendEventMutation() const selectedFeaturePreview = snap.selectedFeaturePreview === '' ? FEATURE_PREVIEWS[0].key : snap.selectedFeaturePreview @@ -53,15 +50,11 @@ const FeaturePreviewModal = () => { const toggleFeature = () => { onUpdateFlag(selectedFeatureKey, !isSelectedFeatureEnabled) - Telemetry.sendEvent( - { - category: 'ui_feature_previews', - action: isSelectedFeatureEnabled ? 'disabled' : 'enabled', - label: selectedFeatureKey, - }, - telemetryProps, - router - ) + sendEvent({ + category: 'ui_feature_previews', + action: isSelectedFeatureEnabled ? 'disabled' : 'enabled', + label: selectedFeatureKey, + }) } function handleCloseFeaturePreviewModal() { diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx index ae28725e3e4..29545a60c92 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyChat.tsx @@ -1,13 +1,11 @@ -import Telemetry from 'lib/telemetry' import { compact, last } from 'lodash' import { ChevronsUpDown, Lightbulb } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' -import { useTelemetryProps } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { SchemaComboBox } from 'components/ui/SchemaComboBox' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi' import { useSchemasForAi } from 'hooks/misc/useSchemasForAi' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' @@ -44,13 +42,11 @@ export const AIPolicyChat = ({ onDiff, clearHistory, }: AIPolicyChatProps) => { - const router = useRouter() + const { profile } = useProfile() const project = useSelectedProject() const selectedOrganization = useSelectedOrganization() - const { profile } = useProfile() - const bottomRef = useRef(null) - const telemetryProps = useTelemetryProps() + const bottomRef = useRef(null) const [selectedSchemas, setSelectedSchemas] = useSchemasForAi(project?.ref!) const [value, setValue] = useState('') const [hasSuggested, setHasSuggested] = useState(false) @@ -62,6 +58,8 @@ export const AIPolicyChat = ({ const name = compact([profile?.first_name, profile?.last_name]).join(' ') const pendingReply = loading && last(messages)?.role === 'user' + const { mutate: sendEvent } = useSendEventMutation() + useEffect(() => { if (!loading) { setValue('') @@ -203,15 +201,11 @@ export const AIPolicyChat = ({ onSubmit={(event) => { event.preventDefault() onSubmit(value) - Telemetry.sendEvent( - { - category: 'rls_editor', - action: 'ai_suggestion_asked', - label: 'rls-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'rls_editor', + action: 'ai_suggestion_asked', + label: 'rls-ai-assistant', + }) }} /> diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx index 4d12138b9f2..8db22ac1c86 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyPre.tsx @@ -6,6 +6,7 @@ import { Button, CodeBlock, cn } from 'ui' import Telemetry from 'lib/telemetry' import { useTelemetryProps } from 'common' import { useRouter } from 'next/router' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' interface AAIPolicyPreProps { onDiff: (s: string) => void @@ -18,6 +19,8 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps) const router = useRouter() const telemetryProps = useTelemetryProps() + const { mutate: sendEvent } = useSendEventMutation() + useEffect(() => { if (!copied) return const timer = setTimeout(() => setCopied(false), 2000) @@ -73,15 +76,11 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps) className={cn('text-foreground-lighter hover:text-foreground', 'transition')} onClick={() => { onDiff(formatted) - Telemetry.sendEvent( - { - category: 'rls_editor', - action: 'ai_suggestion_diffed', - label: 'rls-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'rls_editor', + action: 'ai_suggestion_diffed', + label: 'rls-ai-assistant', + }) }} > @@ -109,15 +108,11 @@ export const AIPolicyPre = ({ onDiff, children, className }: AAIPolicyPreProps) className={cn('text-foreground-lighter hover:text-foreground', 'transition')} onClick={() => { handleCopy(formatted) - Telemetry.sendEvent( - { - category: 'rls_editor', - action: 'ai_suggestion_copied', - label: 'rls-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'rls_editor', + action: 'ai_suggestion_copied', + label: 'rls-ai-assistant', + }) }} > {copied ? ( diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/QueryError.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/QueryError.tsx index a6b44e6e50f..8faa934e563 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/QueryError.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/QueryError.tsx @@ -1,14 +1,12 @@ -import styles from '@ui/layout/ai-icon-animation/ai-icon-animation-style.module.css' import { initial, last } from 'lodash' import { Dispatch, SetStateAction } from 'react' -import { useTelemetryProps } from 'common' +import styles from '@ui/layout/ai-icon-animation/ai-icon-animation-style.module.css' import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils' import { QueryResponseError } from 'data/sql/execute-sql-mutation' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import Telemetry from 'lib/telemetry' -import { useRouter } from 'next/router' import { AlertTitle_Shadcn_, Alert_Shadcn_, @@ -30,15 +28,15 @@ const QueryError = ({ setOpen: Dispatch> onSelectDebug: () => void }) => { - const formattedError = - (error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? [] - // Customers on HIPAA plans should not have access to Supabase AI const organization = useSelectedOrganization() const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug }) const hasHipaaAddon = subscriptionHasHipaaAddon(subscription) - const router = useRouter() - const telemetryProps = useTelemetryProps() + + const { mutate: sendEvent } = useSendEventMutation() + + const formattedError = + (error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? [] return (
@@ -84,15 +82,11 @@ const QueryError = ({ )} onClick={() => { onSelectDebug() - Telemetry.sendEvent( - { - category: 'rls_editor', - action: 'ai_debugger_requested', - label: 'rls-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'rls_editor', + action: 'ai_debugger_requested', + label: 'rls-ai-assistant', + }) }} > Fix with Assistant diff --git a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx index 6f30963a52b..6005512e4e6 100644 --- a/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx +++ b/apps/studio/components/interfaces/Auth/Policies/AIPolicyEditorPanel/index.tsx @@ -1,15 +1,41 @@ import { zodResolver } from '@hookform/resolvers/zod' +import { Monaco } from '@monaco-editor/react' import type { PostgresPolicy } from '@supabase/postgres-meta' +import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' -import { useChat } from 'ai/react' -import { IS_PLATFORM, useParams, useTelemetryProps } from 'common' import { isEqual, uniqBy } from 'lodash' import { FileDiff } from 'lucide-react' import dynamic from 'next/dynamic' -import { useRouter } from 'next/router' +import { useQueryState } from 'nuqs' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' +import * as z from 'zod' + +import { useChat } from 'ai/react' +import { IS_PLATFORM, useParams } from 'common' +import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils' +import { + IStandaloneCodeEditor, + IStandaloneDiffEditor, +} from 'components/interfaces/SQLEditor/SQLEditor.types' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation' +import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' +import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database-policy-update-mutation' +import { databasePoliciesKeys } from 'data/database-policies/keys' +import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' +import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' +import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi' +import { useSchemasForAi } from 'hooks/misc/useSchemasForAi' +import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { BASE_PATH } from 'lib/constants' +import { uuidv4 } from 'lib/helpers' import { Button, Checkbox_Shadcn_, @@ -25,33 +51,6 @@ import { Tabs_Shadcn_, cn, } from 'ui' -import * as z from 'zod' - -import { Monaco } from '@monaco-editor/react' -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils' -import { - IStandaloneCodeEditor, - IStandaloneDiffEditor, -} from 'components/interfaces/SQLEditor/SQLEditor.types' -import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation' -import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query' -import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database-policy-update-mutation' -import { databasePoliciesKeys } from 'data/database-policies/keys' -import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' -import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' -import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { useOrgOptedIntoAi } from 'hooks/misc/useOrgOptedIntoAi' -import { useSchemasForAi } from 'hooks/misc/useSchemasForAi' -import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' -import { useSelectedProject } from 'hooks/misc/useSelectedProject' -import { BASE_PATH } from 'lib/constants' -import { uuidv4 } from 'lib/helpers' -import Telemetry from 'lib/telemetry' -import { useQueryState } from 'nuqs' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { AIPolicyChat } from './AIPolicyChat' import { @@ -96,13 +95,11 @@ export const AIPolicyEditorPanel = memo(function ({ onSelectCancel, authContext, }: AIPolicyEditorPanelProps) { - const router = useRouter() const { ref } = useParams() const queryClient = useQueryClient() const selectedProject = useSelectedProject() const selectedOrganization = useSelectedOrganization() - const telemetryProps = useTelemetryProps() const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') const [editView, setEditView] = useQueryState('view', { defaultValue: 'templates' as const }) @@ -234,6 +231,8 @@ export const AIPolicyEditorPanel = memo(function ({ ) }, [chatMessages, debugThread]) + const { mutate: sendEvent } = useSendEventMutation() + const { mutate: executeMutation, isLoading: isExecuting } = useExecuteSqlMutation({ onSuccess: async () => { // refresh all policies @@ -511,15 +510,11 @@ export const AIPolicyEditorPanel = memo(function ({ onClick={() => { setIncomingChange(undefined) setSelectedDiff(undefined) - Telemetry.sendEvent( - { - category: 'rls_editor', - action: 'ai_suggestion_discarded', - label: 'rls-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'rls_editor', + action: 'ai_suggestion_discarded', + label: 'rls-ai-assistant', + }) }} > Discard @@ -529,15 +524,11 @@ export const AIPolicyEditorPanel = memo(function ({ onClick={() => { acceptChange() setSelectedDiff(undefined) - Telemetry.sendEvent( - { - category: 'rls_editor', - action: 'ai_suggestion_accepted', - label: 'rls-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'rls_editor', + action: 'ai_suggestion_accepted', + label: 'rls-ai-assistant', + }) }} > Accept diff --git a/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx index 63e32944ea6..0ee820fcd3b 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/ChooseChannelPopover/index.tsx @@ -1,12 +1,10 @@ import { zodResolver } from '@hookform/resolvers/zod' -import { useTelemetryProps } from 'common' -import { useRouter } from 'next/router' +import { ChevronDown, ExternalLink } from 'lucide-react' import { Dispatch, SetStateAction, useState } from 'react' import { useForm } from 'react-hook-form' import * as z from 'zod' -import Telemetry from 'lib/telemetry' -import { ChevronDown, ExternalLink } from 'lucide-react' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { Button, FormControl_Shadcn_, @@ -32,8 +30,8 @@ const FormSchema = z.object({ channel: z.string(), isPrivate: z.boolean() }) export const ChooseChannelPopover = ({ config, onChangeConfig }: ChooseChannelPopoverProps) => { const [open, setOpen] = useState(false) - const telemetryProps = useTelemetryProps() - const router = useRouter() + + const { mutate: sendEvent } = useSendEventMutation() const form = useForm>({ mode: 'onBlur', @@ -52,15 +50,11 @@ export const ChooseChannelPopover = ({ config, onChangeConfig }: ChooseChannelPo const onSubmit = () => { setOpen(false) - Telemetry.sendEvent( - { - category: 'realtime_inspector', - action: 'started_listening_to_channel_in_input_channel_popover', - label: 'realtime_inspector_config', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'realtime_inspector', + action: 'started_listening_to_channel_in_input_channel_popover', + label: 'realtime_inspector_config', + }) onChangeConfig({ ...config, channelName: form.getValues('channel'), diff --git a/apps/studio/components/interfaces/Realtime/Inspector/Header.tsx b/apps/studio/components/interfaces/Realtime/Inspector/Header.tsx index d400408ac8b..e953b6b310e 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/Header.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/Header.tsx @@ -1,11 +1,9 @@ import * as Tooltip from '@radix-ui/react-tooltip' import { PlayCircle, StopCircle } from 'lucide-react' import { Dispatch, SetStateAction } from 'react' -import { Button } from 'ui' -import { useTelemetryProps } from 'common' -import Telemetry from 'lib/telemetry' -import { useRouter } from 'next/router' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { Button } from 'ui' import { ChooseChannelPopover } from './ChooseChannelPopover' import { RealtimeFilterPopover } from './RealtimeFilterPopover' import { RealtimeTokensPopover } from './RealtimeTokensPopover' @@ -17,8 +15,7 @@ interface HeaderProps { } export const Header = ({ config, onChangeConfig }: HeaderProps) => { - const telemetryProps = useTelemetryProps() - const router = useRouter() + const { mutate: sendEvent } = useSendEventMutation() return (
@@ -37,15 +34,11 @@ export const Header = ({ config, onChangeConfig }: HeaderProps) => { onChangeConfig({ ...config, enabled: !config.enabled }) if (!config.enabled) { // the user has clicked to start listening - Telemetry.sendEvent( - { - category: 'realtime_inspector', - action: 'started_listening', - label: 'realtime_inspector_config', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'realtime_inspector', + action: 'started_listening', + label: 'realtime_inspector_config', + }) } }} > diff --git a/apps/studio/components/interfaces/Realtime/Inspector/MessageSelection.tsx b/apps/studio/components/interfaces/Realtime/Inspector/MessageSelection.tsx index c519d5c0c84..facba5b2015 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/MessageSelection.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/MessageSelection.tsx @@ -1,13 +1,11 @@ -import { useTelemetryProps } from 'common' -import { useRouter } from 'next/router' +import { X } from 'lucide-react' import { useMemo } from 'react' -import { Button, cn } from 'ui' import CopyButton from 'components/ui/CopyButton' -import Telemetry from 'lib/telemetry' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { Button, cn } from 'ui' import type { LogData } from './Messages.types' import { SelectedRealtimeMessagePanel } from './SelectedRealtimeMessagePanel' -import { X } from 'lucide-react' export interface MessageSelectionProps { log: LogData | null @@ -15,13 +13,12 @@ export interface MessageSelectionProps { } const MessageSelection = ({ log, onClose }: MessageSelectionProps) => { - const telemetryProps = useTelemetryProps() - const router = useRouter() - const selectionText = useMemo(() => { return JSON.stringify(log, null, 2) }, [log]) + const { mutate: sendEvent } = useSendEventMutation() + return (
{ type="default" title="Copy log to clipboard" onClick={() => { - Telemetry.sendEvent( - { - category: 'realtime_inspector', - action: 'copied_message', - label: 'realtime_inspector_results', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'realtime_inspector', + action: 'copied_message', + label: 'realtime_inspector_results', + }) }} />
diff --git a/apps/studio/components/interfaces/Realtime/Inspector/MessagesTable.tsx b/apps/studio/components/interfaces/Realtime/Inspector/MessagesTable.tsx index e59edd8fbcb..5d3083ea4de 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/MessagesTable.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/MessagesTable.tsx @@ -1,14 +1,13 @@ -import { useParams, useTelemetryProps } from 'common' import { isEqual } from 'lodash' -import { ExternalLink, Loader2, Megaphone, MegaphoneIcon } from 'lucide-react' +import { ExternalLink, Loader2, Megaphone } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import DataGrid, { Row } from 'react-data-grid' -import { Button, IconBroadcast, IconDatabaseChanges, IconPresence, cn } from 'ui' +import { useParams } from 'common' import ShimmerLine from 'components/ui/ShimmerLine' -import Telemetry from 'lib/telemetry' -import { useRouter } from 'next/router' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { Button, IconBroadcast, IconDatabaseChanges, IconPresence, cn } from 'ui' import MessageSelection from './MessageSelection' import type { LogData } from './Messages.types' import NoChannelEmptyState from './NoChannelEmptyState' @@ -116,8 +115,8 @@ const MessagesTable = ({ }: MessagesTableProps) => { const [focusedLog, setFocusedLog] = useState(null) const stringData = JSON.stringify(data) - const telemetryProps = useTelemetryProps() - const router = useRouter() + + const { mutate: sendEvent } = useSendEventMutation() useEffect(() => { if (!data) return @@ -185,15 +184,11 @@ const MessagesTable = ({ isRowSelected={false} selectedCellIdx={undefined} onClick={() => { - Telemetry.sendEvent( - { - category: 'realtime_inspector', - action: 'focused-specific-message', - label: 'realtime_inspector_results', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'realtime_inspector', + action: 'focused-specific-message', + label: 'realtime_inspector_results', + }) setFocusedLog(row) }} /> diff --git a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx index 84af983368c..d8b7e5e90ef 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx @@ -1,8 +1,8 @@ -import { useTelemetryProps } from 'common' import { PlusCircle } from 'lucide-react' import Link from 'next/link' -import { useRouter } from 'next/router' import { Dispatch, SetStateAction, useState } from 'react' + +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { Badge, Button, @@ -16,8 +16,6 @@ import { Toggle, cn, } from 'ui' - -import Telemetry from 'lib/telemetry' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { RealtimeConfig } from '../useRealtimeMessages' import { FilterSchema } from './FilterSchema' @@ -32,8 +30,8 @@ export const RealtimeFilterPopover = ({ config, onChangeConfig }: RealtimeFilter const [open, setOpen] = useState(false) const [applyConfigOpen, setApplyConfigOpen] = useState(false) const [tempConfig, setTempConfig] = useState(config) - const telemetryProps = useTelemetryProps() - const router = useRouter() + + const { mutate: sendEvent } = useSendEventMutation() const onOpen = (v: boolean) => { // when opening, copy the outside config into the intermediate one @@ -202,15 +200,11 @@ export const RealtimeFilterPopover = ({ config, onChangeConfig }: RealtimeFilter visible={applyConfigOpen} onCancel={() => setApplyConfigOpen(false)} onConfirm={() => { - Telemetry.sendEvent( - { - category: 'realtime_inspector', - action: 'applied_filters', - label: 'realtime_inspector_config', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'realtime_inspector', + action: 'applied_filters', + label: 'realtime_inspector_config', + }) onChangeConfig(tempConfig) setApplyConfigOpen(false) setOpen(false) diff --git a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx index fc5b93bbf5b..de190251957 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/RealtimeTokensPopover/index.tsx @@ -1,14 +1,12 @@ -import { useTelemetryProps } from 'common' -import { useRouter } from 'next/router' import { Dispatch, SetStateAction, useEffect, useRef } from 'react' import { toast } from 'sonner' import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector' import { useProjectApiQuery } from 'data/config/project-api-query' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { IS_PLATFORM } from 'lib/constants' import { getRoleImpersonationJWT } from 'lib/role-impersonation' -import Telemetry from 'lib/telemetry' import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state' import { RealtimeConfig } from '../useRealtimeMessages' @@ -19,8 +17,8 @@ interface RealtimeTokensPopoverProps { export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokensPopoverProps) => { const { data: settings } = useProjectApiQuery({ projectRef: config.projectRef }) - const telemetryProps = useTelemetryProps() - const router = useRouter() + + const { mutate: sendEvent } = useSendEventMutation() const apiService = settings?.autoApiService const serviceRoleKey = apiService?.service_api_keys.find((x) => x.name === 'service_role key') @@ -44,15 +42,11 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens const isMounted = useRef(false) useEffect(() => { if (isMounted.current) { - Telemetry.sendEvent( - { - category: 'realtime_inspector', - action: 'changed_database_role', - label: 'realtime_inspector_config', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'realtime_inspector', + action: 'changed_database_role', + label: 'realtime_inspector_config', + }) } isMounted.current = true }, [snap.role]) diff --git a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx index 6a0bedafd1b..5e4eec941ae 100644 --- a/apps/studio/components/interfaces/Realtime/Inspector/index.tsx +++ b/apps/studio/components/interfaces/Realtime/Inspector/index.tsx @@ -1,8 +1,7 @@ -import { useParams, useTelemetryProps } from 'common' -import { useRouter } from 'next/router' +import { useParams } from 'common' import { useState } from 'react' -import Telemetry from 'lib/telemetry' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { Header } from './Header' import MessagesTable from './MessagesTable' import { SendMessageModal } from './SendMessageModal' @@ -13,8 +12,6 @@ import { RealtimeConfig, useRealtimeMessages } from './useRealtimeMessages' */ export const RealtimeInspector = () => { const { ref } = useParams() - const telemetryProps = useTelemetryProps() - const router = useRouter() const [sendMessageShown, setSendMessageShown] = useState(false) const [realtimeConfig, setRealtimeConfig] = useState({ @@ -33,6 +30,7 @@ export const RealtimeInspector = () => { enableBroadcast: true, }) + const { mutate: sendEvent } = useSendEventMutation() const { logData, sendMessage } = useRealtimeMessages(realtimeConfig, setRealtimeConfig) return ( @@ -52,15 +50,11 @@ export const RealtimeInspector = () => { visible={sendMessageShown} onSelectCancel={() => setSendMessageShown(false)} onSelectConfirm={(v) => { - Telemetry.sendEvent( - { - category: 'realtime_inspector', - action: 'send_broadcast_message', - label: 'realtime_inspector_results', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'realtime_inspector', + action: 'send_broadcast_message', + label: 'realtime_inspector_results', + }) sendMessage(v.message, v.payload, () => setSendMessageShown(false)) }} /> diff --git a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx index a08386377c2..751b8fc634f 100644 --- a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx +++ b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/AiMessagePre.tsx @@ -15,6 +15,7 @@ import { import Telemetry from 'lib/telemetry' import { DiffType } from '../SQLEditor.types' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' interface AiMessagePreProps { onDiff: (type: DiffType, s: string) => void @@ -27,6 +28,8 @@ export const AiMessagePre = ({ onDiff, children, className }: AiMessagePreProps) const router = useRouter() const telemetryProps = useTelemetryProps() + const { mutate: sendEvent } = useSendEventMutation() + useEffect(() => { if (!copied) return const timer = setTimeout(() => setCopied(false), 2000) @@ -69,15 +72,11 @@ export const AiMessagePre = ({ onDiff, children, className }: AiMessagePreProps) size="tiny" onClick={() => { onDiff(DiffType.Addition, formatted) - Telemetry.sendEvent( - { - category: 'sql_editor_ai_assistant', - action: 'ai_suggestion_inserted', - label: 'sql-editor-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'sql_editor_ai_assistant', + action: 'ai_suggestion_inserted', + label: 'sql-editor-ai-assistant', + }) }} > @@ -95,15 +94,11 @@ export const AiMessagePre = ({ onDiff, children, className }: AiMessagePreProps) size="tiny" onClick={() => { onDiff(DiffType.Modification, formatted) - Telemetry.sendEvent( - { - category: 'sql_editor_ai_assistant', - action: 'ai_suggestion_replaced', - label: 'sql-editor-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'sql_editor_ai_assistant', + action: 'ai_suggestion_replaced', + label: 'sql-editor-ai-assistant', + }) }} > @@ -121,15 +116,11 @@ export const AiMessagePre = ({ onDiff, children, className }: AiMessagePreProps) size="tiny" onClick={() => { handleCopy(formatted) - Telemetry.sendEvent( - { - category: 'sql_editor_ai_assistant', - action: 'ai_suggestion_copied', - label: 'sql-editor-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'sql_editor_ai_assistant', + action: 'ai_suggestion_copied', + label: 'sql-editor-ai-assistant', + }) }} > {copied ? ( diff --git a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx index 2a0ebfa9edd..9a55270116d 100644 --- a/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx +++ b/apps/studio/components/interfaces/SQLEditor/AiAssistantPanel/index.tsx @@ -1,16 +1,16 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Message as MessageType } from 'ai' -import { useChat } from 'ai/react' import { compact, last } from 'lodash' import { Info } from 'lucide-react' -import { useRouter } from 'next/router' import { useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' -import { useParams, useTelemetryProps } from 'common' +import { Message as MessageType } from 'ai' +import { useChat } from 'ai/react' +import { useParams } from 'common' import { SchemaComboBox } from 'components/ui/SchemaComboBox' import { useEntityDefinitionsQuery } from 'data/database/entity-definitions-query' import { useOrganizationUpdateMutation } from 'data/organizations/organization-update-mutation' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage' import { useSchemasForAi } from 'hooks/misc/useSchemasForAi' @@ -18,7 +18,6 @@ import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { BASE_PATH, LOCAL_STORAGE_KEYS, OPT_IN_TAGS } from 'lib/constants' import { useProfile } from 'lib/profile' -import Telemetry from 'lib/telemetry' import uuidv4 from 'lib/uuid' import { AiIconAnimation, @@ -55,13 +54,25 @@ export const AiAssistantPanel = ({ onClose, includeSchemaMetadata, }: AiAssistantPanelProps) => { - const project = useSelectedProject() - const router = useRouter() const { ref } = useParams() const { profile } = useProfile() + const project = useSelectedProject() + const selectedOrganization = useSelectedOrganization() + const bottomRef = useRef(null) + const inputRef = useRef(null) + + const [chatId, setChatId] = useState(uuidv4()) + const [value, setValue] = useState('') + const [isConfirmOptInModalOpen, setIsConfirmOptInModalOpen] = useState(false) const [selectedSchemas, setSelectedSchemas] = useSchemasForAi(project?.ref!) + const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') + const [showAiNotOptimizedWarningSetting, setShowAiNotOptimizedWarningSetting] = + useLocalStorageQuery(LOCAL_STORAGE_KEYS.SHOW_AI_NOT_OPTIMIZED_WARNING(ref as string), true) + const shouldShowNotOptimizedAlert = + selectedOrganization && !includeSchemaMetadata && showAiNotOptimizedWarningSetting + const { data } = useEntityDefinitionsQuery( { schemas: selectedSchemas, @@ -74,7 +85,6 @@ export const AiAssistantPanel = ({ const entityDefinitions = includeSchemaMetadata ? data?.map((def) => def.sql.trim()) : undefined // Use chat id because useChat doesn't have a reset function to clear all messages - const [chatId, setChatId] = useState(uuidv4()) const { messages: chatMessages, append, @@ -97,38 +107,10 @@ export const AiAssistantPanel = ({ ) }, [chatMessages]) - const bottomRef = useRef(null) - const telemetryProps = useTelemetryProps() - - const [value, setValue] = useState('') - const inputRef = useRef(null) - const name = compact([profile?.first_name, profile?.last_name]).join(' ') const pendingReply = isLoading && last(messages)?.role === 'user' - useEffect(() => { - if (!isLoading) { - setValue('') - if (inputRef.current) inputRef.current.focus() - } - - // Try to scroll on each rerender to the bottom - setTimeout( - () => { - if (bottomRef.current) { - bottomRef.current.scrollIntoView({ behavior: 'smooth' }) - } - }, - isLoading ? 100 : 500 - ) - }, [isLoading]) - - const selectedOrganization = useSelectedOrganization() - - const [isConfirmOptInModalOpen, setIsConfirmOptInModalOpen] = useState(false) - - const canUpdateOrganization = useCheckPermissions(PermissionAction.UPDATE, 'organizations') - + const { mutate: sendEvent } = useSendEventMutation() const { mutate: updateOrganization, isLoading: isUpdating } = useOrganizationUpdateMutation() const confirmOptInToShareSchemaData = async () => { @@ -155,11 +137,22 @@ export const AiAssistantPanel = ({ ) } - const [showAiNotOptimizedWarningSetting, setShowAiNotOptimizedWarningSetting] = - useLocalStorageQuery(LOCAL_STORAGE_KEYS.SHOW_AI_NOT_OPTIMIZED_WARNING(ref as string), true) + useEffect(() => { + if (!isLoading) { + setValue('') + if (inputRef.current) inputRef.current.focus() + } - const shouldShowNotOptimizedAlert = - selectedOrganization && !includeSchemaMetadata && showAiNotOptimizedWarningSetting + // Try to scroll on each rerender to the bottom + setTimeout( + () => { + if (bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, + isLoading ? 100 : 500 + ) + }, [isLoading]) return (
@@ -317,15 +310,11 @@ export const AiAssistantPanel = ({ role: 'user', createdAt: new Date(), }) - Telemetry.sendEvent( - { - category: 'sql_editor_ai_assistant', - action: 'ai_suggestion_asked', - label: 'sql-editor-ai-assistant', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'sql_editor_ai_assistant', + action: 'ai_suggestion_asked', + label: 'sql-editor-ai-assistant', + }) }} />
diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 7ffe029f93c..04178cb2ec5 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -78,6 +78,7 @@ import { suffixWithLimit, } from './SQLEditor.utils' import UtilityPanel from './UtilityPanel/UtilityPanel' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' // Load the monaco editor client-side only (does not behave well server-side) const MonacoEditor = dynamic(() => import('./MonacoEditor'), { ssr: false }) @@ -164,6 +165,8 @@ const SQLEditor = () => { ) const isLoading = urlId === 'new' ? false : snippetIsLoading + const { mutate: sendEvent } = useSendEventMutation() + const { mutate: execute, isLoading: isExecuting } = useExecuteSqlMutation({ onSuccess(data, vars) { if (id) snapV2.addResult(id, data.result, vars.autoLimit) @@ -475,15 +478,11 @@ const SQLEditor = () => { } } - Telemetry.sendEvent( - { - category: 'sql_editor', - action: 'ai_suggestion_accepted', - label: debugSolution ? 'debug_snippet' : 'edit_snippet', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'sql_editor', + action: 'ai_suggestion_accepted', + label: debugSolution ? 'debug_snippet' : 'edit_snippet', + }) setSelectedMessage(undefined) setSelectedDiffType(DiffType.Modification) @@ -507,15 +506,11 @@ const SQLEditor = () => { ]) const discardAiHandler = useCallback(() => { - Telemetry.sendEvent( - { - category: 'sql_editor', - action: 'ai_suggestion_rejected', - label: debugSolution ? 'debug_snippet' : 'edit_snippet', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'sql_editor', + action: 'ai_suggestion_rejected', + label: debugSolution ? 'debug_snippet' : 'edit_snippet', + }) setSelectedMessage(undefined) setDebugSolution(undefined) diff --git a/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx b/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx index 8fc4e61e438..f0fc3ed86ef 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLQuickstarts.tsx @@ -3,13 +3,13 @@ import { partition } from 'lodash' import { useRouter } from 'next/router' import { toast } from 'sonner' -import { useParams, useTelemetryProps } from 'common' +import { useParams } from 'common' import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { uuidv4 } from 'lib/helpers' import { useProfile } from 'lib/profile' -import Telemetry from 'lib/telemetry' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { createSqlSnippetSkeletonV2 } from '../SQLEditor.utils' import SQLCard from './SQLCard' @@ -22,13 +22,14 @@ const SQLQuickstarts = () => { const [, quickStart] = partition(SQL_TEMPLATES, { type: 'template' }) const snapV2 = useSqlEditorV2StateSnapshot() - const telemetryProps = useTelemetryProps() const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', { resource: { type: 'sql', owner_id: profile?.id }, subject: { id: profile?.id }, }) + const { mutate: sendEvent } = useSendEventMutation() + const handleNewQuery = async (sql: string, name: string) => { if (!ref) return console.error('Project ref is required') if (!project) return console.error('Project is required') @@ -74,15 +75,11 @@ const SQLQuickstarts = () => { sql={x.sql} onClick={(sql, title) => { handleNewQuery(sql, title) - Telemetry.sendEvent( - { - category: 'quickstart', - action: 'quickstart_clicked', - label: x.title, - }, - telemetryProps, - router - ) + sendEvent({ + category: 'quickstart', + action: 'quickstart_clicked', + label: x.title, + }) }} /> ))} diff --git a/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx b/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx index 8b239a219d7..fd3c18223f6 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx @@ -3,13 +3,13 @@ import { partition } from 'lodash' import { useRouter } from 'next/router' import { toast } from 'sonner' -import { useParams, useTelemetryProps } from 'common' +import { useParams } from 'common' import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { uuidv4 } from 'lib/helpers' import { useProfile } from 'lib/profile' -import Telemetry from 'lib/telemetry' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { createSqlSnippetSkeletonV2 } from '../SQLEditor.utils' import SQLCard from './SQLCard' @@ -22,13 +22,14 @@ const SQLTemplates = () => { const [sql] = partition(SQL_TEMPLATES, { type: 'template' }) const snapV2 = useSqlEditorV2StateSnapshot() - const telemetryProps = useTelemetryProps() const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', { resource: { type: 'sql', owner_id: profile?.id }, subject: { id: profile?.id }, }) + const { mutate: sendEvent } = useSendEventMutation() + const handleNewQuery = async (sql: string, name: string) => { if (!ref) return console.error('Project ref is required') if (!project) return console.error('Project is required') @@ -74,15 +75,11 @@ const SQLTemplates = () => { sql={x.sql} onClick={(sql, title) => { handleNewQuery(sql, title) - Telemetry.sendEvent( - { - category: 'scripts', - action: 'script_clicked', - label: x.title, - }, - telemetryProps, - router - ) + sendEvent({ + category: 'scripts', + action: 'script_clicked', + label: x.title, + }) }} /> ))} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx index 737eb4ca3fb..0e08655db60 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx @@ -1,15 +1,13 @@ import { compact, isObject, isString, map } from 'lodash' +import { ChevronDownIcon, Clipboard, Download } from 'lucide-react' import { markdownTable } from 'markdown-table' -import { useRouter } from 'next/router' import { useMemo, useRef } from 'react' import { CSVLink } from 'react-csv' import { toast } from 'sonner' -import { useTelemetryProps } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { copyToClipboard } from 'lib/helpers' -import Telemetry from 'lib/telemetry' -import { ChevronDownIcon, Clipboard, Download } from 'lucide-react' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { Button, @@ -25,12 +23,12 @@ export type ResultsDropdownProps = { const ResultsDropdown = ({ id }: ResultsDropdownProps) => { const { project } = useProjectContext() - const telemetryProps = useTelemetryProps() const snapV2 = useSqlEditorV2StateSnapshot() const result = snapV2.results?.[id]?.[0] ?? undefined const csvRef = useRef(null) - const router = useRouter() + + const { mutate: sendEvent } = useSendEventMutation() const csvData = useMemo(() => { if (result?.rows) { @@ -68,11 +66,7 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => { function onDownloadCSV() { csvRef.current?.link.click() - Telemetry.sendEvent( - { category: 'sql_editor', action: 'sql_download_csv', label: '' }, - telemetryProps, - router - ) + sendEvent({ category: 'sql_editor', action: 'sql_download_csv', label: '' }) } function onCopyAsMarkdown() { @@ -92,11 +86,7 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => { copyToClipboard(markdownData, () => { toast.success('Copied results to clipboard') - Telemetry.sendEvent( - { category: 'sql_editor', action: 'sql_copy_as_markdown', label: '' }, - telemetryProps, - router - ) + sendEvent({ category: 'sql_editor', action: 'sql_copy_as_markdown', label: '' }) }) } } @@ -109,11 +99,7 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => { copyToClipboard(JSON.stringify(result.rows, null, 2), () => { toast.success('Copied results to clipboard') - Telemetry.sendEvent( - { category: 'sql_editor', action: 'sql_copy_as_json', label: '' }, - telemetryProps, - router - ) + sendEvent({ category: 'sql_editor', action: 'sql_copy_as_json', label: '' }) }) } } diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx index 4d126082987..ba3b44118ec 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseConnectionString.tsx @@ -1,8 +1,7 @@ -import { useParams, useTelemetryProps } from 'common' import { ChevronDown, ExternalLink } from 'lucide-react' -import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' +import { useParams } from 'common' import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import AlertError from 'components/ui/AlertError' @@ -12,9 +11,9 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedProject } from 'hooks/misc/useSelectedProject' import { pluckObjectFields } from 'lib/helpers' -import Telemetry from 'lib/telemetry' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { useDatabaseSettingsStateSnapshot } from 'state/database-settings' import { @@ -58,8 +57,6 @@ interface DatabaseConnectionStringProps { } export const DatabaseConnectionString = ({ appearance }: DatabaseConnectionStringProps) => { - const router = useRouter() - const telemetryProps = useTelemetryProps() const project = useSelectedProject() const { ref: projectRef, connectionString } = useParams() const snap = useDatabaseSettingsStateSnapshot() @@ -93,6 +90,8 @@ export const DatabaseConnectionString = ({ appearance }: DatabaseConnectionStrin const { data: addons, isSuccess: isSuccessAddons } = useProjectAddonsQuery({ projectRef }) const { ipv4: ipv4Addon } = getAddons(addons?.selected_addons ?? []) + const { mutate: sendEvent } = useSendEventMutation() + const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at'] const emptyState = { db_user: '', db_host: '', db_port: '', db_name: '' } const connectionInfo = pluckObjectFields(selectedDatabase || emptyState, DB_FIELDS) @@ -103,15 +102,11 @@ export const DatabaseConnectionString = ({ appearance }: DatabaseConnectionStrin const handleCopy = (id: string) => { const labelValue = CONNECTION_TYPES.find((type) => type.id === id)?.label - Telemetry.sendEvent( - { - category: 'settings', - action: 'copy_connection_string', - label: labelValue ? labelValue : '', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'settings', + action: 'copy_connection_string', + label: labelValue ? labelValue : '', + }) } const connectionStrings = diff --git a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx index 08de8798a29..aaa0deef01d 100644 --- a/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DatabaseSettings/DatabaseSettings.tsx @@ -1,7 +1,6 @@ -import { useRouter } from 'next/router' import { useEffect, useRef, useState } from 'react' -import { useParams, useTelemetryProps } from 'common' +import { useParams } from 'common' import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils' import AlertError from 'components/ui/AlertError' import DatabaseSelector from 'components/ui/DatabaseSelector' @@ -10,12 +9,11 @@ import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { usePoolingConfigurationQuery } from 'data/database/pooling-configuration-query' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { pluckObjectFields } from 'lib/helpers' -import Telemetry from 'lib/telemetry' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { useDatabaseSettingsStateSnapshot } from 'state/database-settings' -import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Input } from 'ui' -import { WarningIcon } from 'ui' +import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Input, WarningIcon } from 'ui' import { DefaultSessionModeNotice, IPv4AddonDirectConnectionNotice, @@ -25,9 +23,7 @@ import { UsePoolerCheckbox } from '../UsePoolerCheckbox' import ResetDbPassword from './ResetDbPassword' const DatabaseSettings = () => { - const router = useRouter() const { ref: projectRef, connectionString } = useParams() - const telemetryProps = useTelemetryProps() const snap = useDatabaseSettingsStateSnapshot() const state = useDatabaseSelectorStateSnapshot() @@ -57,6 +53,8 @@ const DatabaseSettings = () => { isError: isErrorAddons, isSuccess: isSuccessAddons, } = useProjectAddonsQuery({ projectRef }) + const { mutate: sendEvent } = useSendEventMutation() + const error = readReplicasError || poolingInfoError || addOnsError const isLoading = isLoadingReadReplicas || isLoadingPoolingInfo || isLoadingAddons const isError = isErrorReadReplicas || isErrorPoolingInfo || isErrorAddons @@ -84,15 +82,11 @@ const DatabaseSettings = () => { : dbConnectionInfo const handleCopy = (labelValue?: string) => - Telemetry.sendEvent( - { - category: 'settings', - action: 'copy_connection_string', - label: labelValue ? labelValue : '', - }, - telemetryProps, - router - ) + sendEvent({ + category: 'settings', + action: 'copy_connection_string', + label: labelValue ? labelValue : '', + }) useEffect(() => { if (connectionString !== undefined && connectionStringsRef.current !== undefined) { diff --git a/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx b/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx index 20c3c60ca06..9869669b92c 100644 --- a/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx +++ b/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx @@ -6,14 +6,13 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { object, string } from 'yup' -import { useTelemetryProps } from 'common' import AlertError from 'components/ui/AlertError' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' import { useMfaChallengeAndVerifyMutation } from 'data/profile/mfa-challenge-and-verify-mutation' import { useMfaListFactorsQuery } from 'data/profile/mfa-list-factors-query' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSignOut } from 'lib/auth' import { getReturnToPath } from 'lib/gotrue' -import Telemetry from 'lib/telemetry' import { Button, Form, Input } from 'ui' const signInSchema = object({ @@ -24,9 +23,10 @@ const SignInMfaForm = () => { const router = useRouter() const signOut = useSignOut() const queryClient = useQueryClient() - const telemetryProps = useTelemetryProps() const [selectedFactor, setSelectedFactor] = useState(null) + const { mutate: sendEvent } = useSendEventMutation() + const { data: factors, error: factorsError, @@ -40,11 +40,7 @@ const SignInMfaForm = () => { isSuccess, } = useMfaChallengeAndVerifyMutation({ onSuccess: async () => { - Telemetry.sendEvent( - { category: 'account', action: 'sign_in', label: '' }, - telemetryProps, - router - ) + sendEvent({ category: 'account', action: 'sign_in', label: '' }) await queryClient.resetQueries() router.push(getReturnToPath()) }, @@ -149,9 +145,8 @@ const SignInMfaForm = () => { onClick={() => setSelectedFactor(factors.totp.find((f) => f.id !== selectedFactor?.id)!) } - >{`Authenticate using ${ - factors.totp.find((f) => f.id !== selectedFactor?.id)?.friendly_name - }?`} + >{`Authenticate using ${factors.totp.find((f) => f.id !== selectedFactor?.id) + ?.friendly_name}?`} )}
  • diff --git a/apps/studio/data/telemetry/send-event-mutation.ts b/apps/studio/data/telemetry/send-event-mutation.ts new file mode 100644 index 00000000000..432f10b0dd9 --- /dev/null +++ b/apps/studio/data/telemetry/send-event-mutation.ts @@ -0,0 +1,121 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { isBrowser } from 'common' +import { handleError, post } from 'data/fetchers' +import { useFlag } from 'hooks/ui/useFlag' +import { useRouter } from 'next/router' +import type { ResponseError } from 'types' + +type SendEventGA = { + action: string + category: string + label: string + value: string + page_referrer: string + page_title: string + page_location: string + ga: { + screen_resolution: string + language: string + } +} + +type SendEventPH = { + action: string + page_url: string + page_title: string + pathname: string + ph: { + language: string + referrer: string + userAgent: string + search: string + viewport_height: number + viewport_width: number + } + custom_properties: { [key: string]: string } +} + +export type SendEventVariables = { + action: string + category: string + label: string + value?: string +} + +type SendEventPayload = any + +export async function sendEvent(type: 'GA' | 'PH', body: SendEventPayload) { + const headers = type === 'PH' ? { Version: '2' } : undefined + const { data, error } = await post(`/platform/telemetry/event`, { body, headers }) + if (error) handleError(error) + return data +} + +type SendEventData = Awaited> + +export const useSendEventMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const router = useRouter() + const usePostHogParameters = useFlag('enablePosthogChanges') + + const payload = usePostHogParameters + ? ({ + page_url: window.location.href, + page_title: document?.title, + pathname: router.pathname, + ph: { + language: router?.locale ?? 'en-US', + referrer: document?.referrer, + userAgent: navigator.userAgent, + search: window.location.search, + viewport_height: isBrowser ? window.innerHeight : 0, + viewport_width: isBrowser ? window.innerWidth : 0, + }, + custom_properties: {}, + } as SendEventPH) + : ({ + page_referrer: document?.referrer, + page_title: document?.title, + page_location: router.asPath.split('#')[0], + ga: { + screen_resolution: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined, + language: router?.locale ?? 'en-US', + }, + } as SendEventGA) + + return useMutation( + (vars) => { + const { action, ...otherVars } = vars + const type = usePostHogParameters ? 'PH' : 'GA' + const body = usePostHogParameters + ? ({ + action, + ...(payload as Omit), + custom_properties: otherVars, + } as SendEventPH) + : ({ ...vars, ...payload } as SendEventGA) + return sendEvent(type, body) + }, + { + async onSuccess(data, variables, context) { + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to send Telemetry event: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + } + ) +} diff --git a/apps/studio/lib/profile.tsx b/apps/studio/lib/profile.tsx index 2d10bc3afb8..be6886aa9aa 100644 --- a/apps/studio/lib/profile.tsx +++ b/apps/studio/lib/profile.tsx @@ -1,5 +1,4 @@ import { useIsLoggedIn, useTelemetryProps } from 'common' -import { useRouter } from 'next/router' import { PropsWithChildren, createContext, useContext, useMemo } from 'react' import { toast } from 'sonner' @@ -7,6 +6,7 @@ import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useProfileCreateMutation } from 'data/profile/profile-create-mutation' import { useProfileQuery } from 'data/profile/profile-query' import type { Profile } from 'data/profile/types' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import Telemetry from 'lib/telemetry' import type { ResponseError } from 'types' @@ -27,18 +27,14 @@ export const ProfileContext = createContext({ }) export const ProfileProvider = ({ children }: PropsWithChildren<{}>) => { - const router = useRouter() + const isLoggedIn = useIsLoggedIn() const telemetryProps = useTelemetryProps() - const isLoggedIn = useIsLoggedIn() + const { mutate: sendEvent } = useSendEventMutation() const { mutate: createProfile, isLoading: isCreatingProfile } = useProfileCreateMutation({ async onSuccess() { - Telemetry.sendEvent( - { category: 'conversion', action: 'sign_up', label: '' }, - telemetryProps, - router - ) + sendEvent({ category: 'conversion', action: 'sign_up', label: '' }) }, onError() { toast.error('Failed to create your profile. Please refresh to try again.') diff --git a/apps/studio/lib/telemetry.ts b/apps/studio/lib/telemetry.ts index 3efdfed495b..4865f8c7232 100644 --- a/apps/studio/lib/telemetry.ts +++ b/apps/studio/lib/telemetry.ts @@ -1,5 +1,4 @@ import { Sha256 } from '@aws-crypto/sha256-browser' -import type { NextRouter } from 'next/router' import { post } from 'lib/common/fetch' import { API_URL, IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants' @@ -10,49 +9,6 @@ export interface TelemetryProps { language: string } -/** - * Sends a telemetry event to Logflare for tracking by the product team. - */ -const sendEvent = ( - event: { - category: string - action: string - label: string - value?: string - }, - gaProps: TelemetryProps, - router: NextRouter -) => { - if (!IS_PLATFORM) return - - const consent = - typeof window !== 'undefined' - ? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) - : null - if (consent !== 'true') return - - const { category, action, label, value } = event - - // remove # section from router.asPath as it - // often includes sensitive information - // such as access/refresh tokens - const page_location = router.asPath.split('#')[0] - - return post(`${API_URL}/telemetry/event`, { - action: action, - category: category, - label: label, - value: value, - page_referrer: document?.referrer, - page_title: document?.title, - page_location, - ga: { - screen_resolution: gaProps?.screenResolution, - language: gaProps?.language, - }, - }) -} - /** * TODO: GA4 doesn't have identify method. * We may or may not need gaClientId here. Confirm later @@ -88,7 +44,6 @@ export const getAnonId = async (id: string) => { } const Telemetry = { - sendEvent, sendIdentify, } diff --git a/apps/studio/pages/sign-in-mfa.tsx b/apps/studio/pages/sign-in-mfa.tsx index efc949e9743..4a37aa30fd3 100644 --- a/apps/studio/pages/sign-in-mfa.tsx +++ b/apps/studio/pages/sign-in-mfa.tsx @@ -3,19 +3,18 @@ import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import { toast } from 'sonner' -import { useTelemetryProps } from 'common' import SignInMfaForm from 'components/interfaces/SignIn/SignInMfaForm' import SignInLayout from 'components/layouts/SignInLayout/SignInLayout' import { Loading } from 'components/ui/Loading' +import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { auth, buildPathWithParams, getAccessToken, getReturnToPath } from 'lib/gotrue' -import Telemetry from 'lib/telemetry' import type { NextPageWithLayout } from 'types' const SignInMfaPage: NextPageWithLayout = () => { const router = useRouter() - const telemetryProps = useTelemetryProps() const queryClient = useQueryClient() const [loading, setLoading] = useState(true) + const { mutate: sendEvent } = useSendEventMutation() // This useEffect redirects the user to MFA if they're already halfway signed in useEffect(() => { @@ -42,15 +41,9 @@ const SignInMfaPage: NextPageWithLayout = () => { } if (data.currentLevel === data.nextLevel) { - Telemetry.sendEvent( - { category: 'account', action: 'sign_in', label: '' }, - telemetryProps, - router - ) + sendEvent({ category: 'account', action: 'sign_in', label: '' }) await queryClient.resetQueries() - router.push(getReturnToPath()) - return } if (data.currentLevel !== data.nextLevel) { diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 2a23884cd7c..70fb935effa 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -95,10 +95,6 @@ export interface paths { /** Return quotes for different billing plans for a specific product */ get: operations['ProductsController_listResources'] } - '/partners/vercel/webhooks': { - /** Webhooks endpoint */ - post: operations['WebhooksController_processWebhook'] - } '/platform/auth/{ref}/config': { /** Gets GoTrue config */ get: operations['GoTrueConfigController_getGoTrueConfig'] @@ -1077,15 +1073,31 @@ export interface paths { } '/platform/telemetry/event': { /** Sends analytics server event */ - post: operations['TelemetryEventController_sendServerEvent'] + post: operations['TelemetryEventController_sendServerEventV2'] + } + '/platform/telemetry/groups/identify': { + /** Send analytics group identify event */ + post: operations['TelemetryGroupsController_groupIdentify'] + } + '/platform/telemetry/groups/reset': { + /** Send analytics group reset event */ + post: operations['TelemetryGroupsController_groupReset'] } '/platform/telemetry/identify': { /** Send analytics identify event */ - post: operations['TelemetryIdentifyController_identify'] + post: operations['TelemetryIdentifyController_identifyV2'] } '/platform/telemetry/page': { /** Send server page event */ - post: operations['TelemetryPageController_sendServerPage'] + post: operations['TelemetryPageController_sendServerPageV2'] + } + '/platform/telemetry/page-leave': { + /** Send analytics page leave event */ + post: operations['TelemetryPageLeaveController_pageLeave'] + } + '/platform/telemetry/reset': { + /** Reset analytics */ + post: operations['TelemetryResetController_reset'] } '/platform/tos/fly': { /** Redirects to Fly sso flow */ @@ -1959,6 +1971,8 @@ export interface paths { post: operations['v1-create-a-project'] } '/v1/projects/{ref}': { + /** Gets a specific project that belongs to the authenticated user */ + get: operations['v1-get-project'] /** Deletes the given project */ delete: operations['v1-delete-a-project'] } @@ -5569,16 +5583,64 @@ export interface components { page_title?: string value?: string } + TelemetryEventBodyV2: { + action: string + custom_properties: Record + page_title: string + page_url: string + pathname: string + ph: components['schemas']['TelemetryEventPostHog'] + } + TelemetryEventPostHog: { + language: string + referrer: string + search: string + userAgent: string + viewport_height: number + viewport_width: number + } + TelemetryGroupsIdentityBody: { + organization_slug?: string + project_ref?: string + } + TelemetryGroupsResetBody: { + reset_organization?: boolean + reset_project?: boolean + } TelemetryIdentifyBody: { ga?: components['schemas']['GoogleAnalyticBody'] user: components['schemas']['IdentifyUserBody'] } + TelemetryIdentifyBodyV2: { + organization_slug?: string + project_ref?: string + user_id: string + } TelemetryPageBody: { ga?: components['schemas']['GoogleAnalyticBody'] referrer: string route?: string title: string } + TelemetryPageBodyV2: { + page_title: string + page_url: string + pathname: string + ph: components['schemas']['TelemetryPagePostHog'] + } + TelemetryPageLeaveBody: { + page_title: string + page_url: string + pathname: string + } + TelemetryPagePostHog: { + language: string + referrer: string + search: string + userAgent: string + viewport_height: number + viewport_width: number + } ThirdPartyAuth: { custom_jwks?: unknown id: string @@ -6959,19 +7021,6 @@ export interface operations { } } } - /** Webhooks endpoint */ - WebhooksController_processWebhook: { - parameters: { - header: { - 'x-vercel-signature': string - } - } - responses: { - 201: { - content: never - } - } - } /** Gets GoTrue config */ GoTrueConfigController_getGoTrueConfig: { parameters: { @@ -13603,10 +13652,10 @@ export interface operations { } } /** Sends analytics server event */ - TelemetryEventController_sendServerEvent: { + TelemetryEventController_sendServerEventV2: { requestBody: { content: { - 'application/json': components['schemas']['TelemetryEventBody'] + 'application/json': components['schemas']['TelemetryEventBodyV2'] } } responses: { @@ -13619,11 +13668,45 @@ export interface operations { } } } - /** Send analytics identify event */ - TelemetryIdentifyController_identify: { + /** Send analytics group identify event */ + TelemetryGroupsController_groupIdentify: { requestBody: { content: { - 'application/json': components['schemas']['TelemetryIdentifyBody'] + 'application/json': components['schemas']['TelemetryGroupsIdentityBody'] + } + } + responses: { + 201: { + content: never + } + /** @description Failed to send analytics group identify event */ + 500: { + content: never + } + } + } + /** Send analytics group reset event */ + TelemetryGroupsController_groupReset: { + requestBody: { + content: { + 'application/json': components['schemas']['TelemetryGroupsResetBody'] + } + } + responses: { + 201: { + content: never + } + /** @description Failed to send analytics group reset event */ + 500: { + content: never + } + } + } + /** Send analytics identify event */ + TelemetryIdentifyController_identifyV2: { + requestBody: { + content: { + 'application/json': components['schemas']['TelemetryIdentifyBodyV2'] } } responses: { @@ -13637,10 +13720,10 @@ export interface operations { } } /** Send server page event */ - TelemetryPageController_sendServerPage: { + TelemetryPageController_sendServerPageV2: { requestBody: { content: { - 'application/json': components['schemas']['TelemetryPageBody'] + 'application/json': components['schemas']['TelemetryPageBodyV2'] } } responses: { @@ -13653,6 +13736,35 @@ export interface operations { } } } + /** Send analytics page leave event */ + TelemetryPageLeaveController_pageLeave: { + requestBody: { + content: { + 'application/json': components['schemas']['TelemetryPageLeaveBody'] + } + } + responses: { + 201: { + content: never + } + /** @description Failed to send analytics page leave event */ + 500: { + content: never + } + } + } + /** Reset analytics */ + TelemetryResetController_reset: { + responses: { + 201: { + content: never + } + /** @description Failed to reset analytics */ + 500: { + content: never + } + } + } /** Redirects to Fly sso flow */ TermsOfServiceController_flyTosAccepted: { parameters: { @@ -14865,6 +14977,26 @@ export interface operations { } } } + /** Gets a specific project that belongs to the authenticated user */ + 'v1-get-project': { + parameters: { + path: { + /** @description Project ref */ + ref: string + } + } + responses: { + 200: { + content: { + 'application/json': components['schemas']['V1ProjectResponse'] + } + } + /** @description Failed to retrieve project */ + 500: { + content: never + } + } + } /** Deletes the given project */ 'v1-delete-a-project': { parameters: {