Refactor telemetry event into RQ hook, and update to support PH

This commit is contained in:
Joshen Lim committed 2024-10-08 21:05:18 +08:00
1 parent 57d8f92176
commit cc420c18ca
26 files changed
+553 -493

No files matched your search

@@ -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() {
@@ -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<HTMLDivElement>(null)
const telemetryProps = useTelemetryProps()
const bottomRef = useRef<HTMLDivElement>(null)
const [selectedSchemas, setSelectedSchemas] = useSchemasForAi(project?.ref!)
const [value, setValue] = useState<string>('')
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',
})
}}
/>
</div>
@@ -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',
})
}}
>
<FileDiff className={cn('h-4 w-4')} />
@@ -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 ? (
@@ -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<SetStateAction<boolean>>
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 (
<div className="flex flex-col gap-y-3 px-5">
@@ -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
@@ -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
@@ -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<z.infer<typeof FormSchema>>({
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'),
@@ -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 (
<div className="flex flex-row h-14 gap-2.5 items-center px-4">
@@ -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',
})
}
}}
>
@@ -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 (
<div
className={cn(
@@ -76,15 +73,11 @@ const MessageSelection = ({ log, onClose }: MessageSelectionProps) => {
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',
})
}}
/>
</div>
@@ -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<LogData | null>(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)
}}
/>
@@ -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)
@@ -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])
@@ -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<RealtimeConfig>({
@@ -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))
}}
/>
@@ -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',
})
}}
>
<InsertCode className="h-4 w-4 text-foreground-light" strokeWidth={1.5} />
@@ -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',
})
}}
>
<ReplaceCode className="h-4 w-4 text-foreground-light" strokeWidth={1.5} />
@@ -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 ? (
@@ -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<HTMLDivElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
const [chatId, setChatId] = useState(uuidv4())
const [value, setValue] = useState<string>('')
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<HTMLDivElement>(null)
const telemetryProps = useTelemetryProps()
const [value, setValue] = useState<string>('')
const inputRef = useRef<HTMLTextAreaElement>(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 (
<div className="flex flex-col h-full border-l border-control">
@@ -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',
})
}}
/>
</div>
@@ -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)
@@ -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,
})
}}
/>
))}
@@ -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,
})
}}
/>
))}
@@ -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<CSVLink & HTMLAnchorElement & { link: HTMLAnchorElement }>(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: '' })
})
}
}
@@ -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 =
@@ -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) {
@@ -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<Factor | null>(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
}?`}</a>
>{`Authenticate using ${factors.totp.find((f) => f.id !== selectedFactor?.id)
?.friendly_name}?`}</a>
</li>
)}
<li>
@@ -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<ReturnType<typeof sendEvent>>
export const useSendEventMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<SendEventData, ResponseError, SendEventVariables>,
'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<SendEventData, ResponseError, SendEventVariables>(
(vars) => {
const { action, ...otherVars } = vars
const type = usePostHogParameters ? 'PH' : 'GA'
const body = usePostHogParameters
? ({
action,
...(payload as Omit<SendEventPH, 'action'>),
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,
}
)
}
+4 -8
View File
@@ -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<ProfileContextType>({
})
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.')
-45
View File
@@ -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,
}
+3 -10
View File
@@ -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) {
+159 -27
View File
@@ -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<string, never>
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: {