Repository: supabase/supabase
Pull request #34569: chore: UI lib - update telemetry
Author: login: MildTomato
Created: 2025-03-31T11:00:32Z | Merged: 2025-03-31T13:51:19Z

## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file.

YES/NO

## What kind of change does this PR introduce?

Bug fix, feature, docs update, ...

## What is the current behavior?

Please link any relevant issues here.

## What is the new behavior?

Feel free to include screenshots if it includes visual changes.

## Additional context

Add any other context or screenshots.



Diff:
diff --git a/apps/ui-library/.env b/apps/ui-library/.env
index 69c207b2cd1b2..47ab9bf34edbf 100644
--- a/apps/ui-library/.env
+++ b/apps/ui-library/.env
@@ -1,4 +1,5 @@
 # Default to local supabase for local development
 NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
 NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
-NEXT_PUBLIC_BASE_PATH=/ui
\ No newline at end of file
+NEXT_PUBLIC_BASE_PATH=/ui
+NEXT_PUBLIC_API_URL="http://localhost:8080/platform"
\ No newline at end of file
diff --git a/apps/ui-library/app/(app)/layout.tsx b/apps/ui-library/app/(app)/layout.tsx
index 325267198397a..e470668216562 100644
--- a/apps/ui-library/app/(app)/layout.tsx
+++ b/apps/ui-library/app/(app)/layout.tsx
@@ -1,5 +1,6 @@
 import { Sidebar } from '@/components/sidebar'
 import { SiteFooter } from '@/components/site-footer'
+import { TelemetryWrapper } from './telemetry-wrapper'
 
 interface AppLayoutProps {
   children: React.ReactNode
@@ -24,6 +25,7 @@ export default function AppLayout({ children }: AppLayoutProps) {
         </main>
       </div>
       <SiteFooter />
+      <TelemetryWrapper />
     </>
   )
 }
diff --git a/apps/ui-library/app/(app)/telemetry-wrapper.tsx b/apps/ui-library/app/(app)/telemetry-wrapper.tsx
new file mode 100644
index 0000000000000..5460e46156b99
--- /dev/null
+++ b/apps/ui-library/app/(app)/telemetry-wrapper.tsx
@@ -0,0 +1,16 @@
+'use client'
+
+import { API_URL } from '@/lib/constants'
+import { IS_PLATFORM, PageTelemetry } from 'common'
+import { useConsent } from 'ui-patterns/ConsentToast'
+
+export const TelemetryWrapper = () => {
+  const { hasAcceptedConsent } = useConsent()
+  return (
+    <PageTelemetry
+      API_URL={API_URL}
+      hasAcceptedConsent={hasAcceptedConsent}
+      enabled={IS_PLATFORM}
+    />
+  )
+}
diff --git a/apps/ui-library/app/Providers.tsx b/apps/ui-library/app/Providers.tsx
index 97e0ebdbc72b3..d6083701403da 100644
--- a/apps/ui-library/app/Providers.tsx
+++ b/apps/ui-library/app/Providers.tsx
@@ -1,13 +1,14 @@
 'use client'
 
-import { AuthProvider } from 'common'
 import { Provider as JotaiProvider } from 'jotai'
 import { ThemeProvider as NextThemesProvider } from 'next-themes'
 import { ThemeProviderProps } from 'next-themes/dist/types'
 
 import { FrameworkProvider } from '@/context/framework-context'
 import { MobileMenuProvider } from '@/context/mobile-menu-context'
+import { AuthProvider } from 'common'
 import { TooltipProvider } from 'ui'
+import { useConsent } from 'ui-patterns/ConsentToast'
 
 export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
   return (
diff --git a/apps/ui-library/components/command-copy-button.tsx b/apps/ui-library/components/command-copy-button.tsx
index d040a80a10d40..a5fff1b356dd8 100644
--- a/apps/ui-library/components/command-copy-button.tsx
+++ b/apps/ui-library/components/command-copy-button.tsx
@@ -2,10 +2,13 @@
 
 import { Check, Copy } from 'lucide-react'
 import { useEffect, useState } from 'react'
+
+import { useSendTelemetryEvent } from 'lib/telemetry'
 import { Button_Shadcn_ } from 'ui'
 
 export function CommandCopyButton({ command }: { command: string }) {
   const [copied, setCopied] = useState(false)
+  const sendTelemetryEvent = useSendTelemetryEvent()
 
   useEffect(() => {
     if (copied) {
@@ -14,13 +17,56 @@ export function CommandCopyButton({ command }: { command: string }) {
     }
   }, [copied])
 
+  const parseCommandForTelemetry = (cmd: string) => {
+    // Extract framework from URL (e.g., 'nextjs' from 'password-based-auth-nextjs.json')
+    const frameworkMatch = cmd.match(/\/ui\/r\/.*?-(\w+)\.json/)
+    const framework = frameworkMatch
+      ? (frameworkMatch[1] as 'nextjs' | 'react-router' | 'tanstack' | 'react')
+      : 'nextjs'
+
+    // Extract package manager from command prefix (npx, pnpm, yarn, bun)
+    const packageManager = cmd.startsWith('npx')
+      ? ('npm' as const)
+      : cmd.startsWith('pnpm')
+        ? ('pnpm' as const)
+        : cmd.startsWith('yarn')
+          ? ('yarn' as const)
+          : cmd.startsWith('bun')
+            ? ('bun' as const)
+            : ('npm' as const)
+
+    // Extract template title from URL (e.g., 'password-based-auth' from 'password-based-auth-nextjs.json')
+    const titleMatch = cmd.match(/\/ui\/r\/(.*?)-\w+\.json/)
+    const title = titleMatch ? titleMatch[1] : ''
+
+    return {
+      framework,
+      packageManager,
+      title,
+    }
+  }
+
   return (
     <Button_Shadcn_
       variant="ghost"
       size="icon"
       onClick={() => {
+        // Copy command to clipboard
         navigator.clipboard.writeText(command)
         setCopied(true)
+
+        // Parse command and send telemetry event
+        const { framework, packageManager, title } = parseCommandForTelemetry(command)
+
+        sendTelemetryEvent({
+          action: 'supabase_ui_command_copy_button_clicked',
+          properties: {
+            templateTitle: title,
+            command: command,
+            framework,
+            packageManager,
+          },
+        })
       }}
     >
       {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
diff --git a/apps/ui-library/lib/constants.ts b/apps/ui-library/lib/constants.ts
new file mode 100644
index 0000000000000..9b2dc13741b33
--- /dev/null
+++ b/apps/ui-library/lib/constants.ts
@@ -0,0 +1 @@
+export const API_URL = process.env.NEXT_PUBLIC_API_URL!
diff --git a/apps/ui-library/lib/telemetry.ts b/apps/ui-library/lib/telemetry.ts
new file mode 100644
index 0000000000000..804549688ccdb
--- /dev/null
+++ b/apps/ui-library/lib/telemetry.ts
@@ -0,0 +1,19 @@
+'use client'
+
+import { usePathname } from 'next/navigation'
+import { useCallback } from 'react'
+
+import { sendTelemetryEvent } from 'common'
+import { TelemetryEvent } from 'common/telemetry-constants'
+import { API_URL } from 'lib/constants'
+
+export function useSendTelemetryEvent() {
+  const pathname = usePathname()
+
+  return useCallback(
+    (event: TelemetryEvent) => {
+      return sendTelemetryEvent(API_URL, event, pathname)
+    },
+    [pathname]
+  )
+}
diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts
index 6182dba9e30e4..83cdcdca39227 100644
--- a/packages/common/telemetry-constants.ts
+++ b/packages/common/telemetry-constants.ts
@@ -1223,6 +1223,23 @@ export interface EdgeFunctionTestSidePanelOpenedEvent {
   }
 }
 
+/**
+ * User copied the command for a Supabase UI component.
+ *
+ * @group Events
+ * @source supabase-ui
+ * @page /ui/docs/{framework}/{templateTitle}
+ */
+export interface SupabaseUiCommandCopyButtonClickedEvent {
+  action: 'supabase_ui_command_copy_button_clicked'
+  properties: {
+    templateTitle: string
+    command: string
+    framework: 'nextjs' | 'react-router' | 'tanstack' | 'react'
+    packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun'
+  }
+}
+
 /**
  * @hidden
  */
@@ -1297,3 +1314,4 @@ export type TelemetryEvent =
   | EdgeFunctionDeployUpdatesButtonClickedEvent
   | EdgeFunctionTestSendButtonClickedEvent
   | EdgeFunctionTestSidePanelOpenedEvent
+  | SupabaseUiCommandCopyButtonClickedEvent
diff --git a/turbo.json b/turbo.json
index adcba386ab347..61988aaa14a03 100644
--- a/turbo.json
+++ b/turbo.json
@@ -130,6 +130,7 @@
         "NEXT_PUBLIC_APP_URL",
         "NEXT_PUBLIC_VERCEL_TARGET_ENV",
         "NEXT_PUBLIC_VERCEL_BRANCH_URL",
+        "NEXT_PUBLIC_API_URL",
         "NODE_ENV",
         "HIDE_TAILWIND_INDICATOR",
         // These envs are used in the packages


Reviews:
user: login: ivasilov
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-31T13:21:55Z
user: login: pamelachia
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:25:08Z
user: login: pamelachia
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:29:58Z
user: login: 4L3k51
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T13:42:53Z

Review comments:
diff_hunk: @@ -14,13 +17,56 @@ export function CommandCopyButton({ command }: { command: string }) {
     }
   }, [copied])
 
+  const parseCommandForTelemetry = (cmd: string) => {
+    // Extract framework from URL (e.g., 'nextjs' from 'password-based-auth-nextjs.json')
+    const frameworkMatch = cmd.match(/\/ui\/r\/.*?-(\w+)\.json/)
+    const framework = frameworkMatch
+      ? (frameworkMatch[1] as 'nextjs' | 'react-router' | 'tanstack' | 'react')
+      : 'nextjs'
+
+    // Extract package manager from command prefix (npx, pnpm, yarn, bun)
+    const packageManager = cmd.startsWith('npx')
+      ? ('npm' as const)
+      : cmd.startsWith('pnpm')
+        ? ('pnpm' as const)
+        : cmd.startsWith('yarn')
+          ? ('yarn' as const)
+          : cmd.startsWith('bun')
+            ? ('bun' as const)
+            : ('npm' as const)
+
+    // Extract template title from URL (e.g., 'password-based-auth' from 'password-based-auth-nextjs.json')
+    const titleMatch = cmd.match(/\/ui\/r\/(.*?)-\w+\.json/)
+    const title = titleMatch ? titleMatch[1] : ''
+
+    return {
+      framework,
+      packageManager,
+      title,
+    }
+  }
+
   return (
     <Button_Shadcn_
       variant="ghost"
       size="icon"
       onClick={() => {
+        // Copy command to clipboard
         navigator.clipboard.writeText(command)
         setCopied(true)
+
+        // Parse command and send telemetry event
+        const { framework, packageManager, title } = parseCommandForTelemetry(command)
+
+        sendTelemetryEvent({
+          action: 'supabase_ui_command_copied',
path: apps/ui-library/components/command-copy-button.tsx
user: login: pamelachia
body: ```suggestion
          action: 'supabase_ui_command_copy_button_clicked',
```
created_at: 2025-03-31T13:24:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1223,6 +1223,23 @@ export interface EdgeFunctionTestSidePanelOpenedEvent {
   }
 }
 
+/**
+ * User copied the command for a Supabase UI component.
+ *
+ * @group Events
+ * @source supabase-ui
+ * @page /ui/docs/{framework}/{templateTitle}
+ */
+export interface SupabaseUiCommandCopiedEvent {
+  action: 'supabase_ui_command_copied'
path: packages/common/telemetry-constants.ts
user: login: pamelachia
body: ```suggestion
export interface SupabaseUiCommandCopyButtonClickedEvent {
  action: 'supabase_ui_command_copy_button_clicked'
```
created_at: 2025-03-31T13:24:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1297,3 +1314,4 @@ export type TelemetryEvent =
   | EdgeFunctionDeployUpdatesButtonClickedEvent
   | EdgeFunctionTestSendButtonClickedEvent
   | EdgeFunctionTestSidePanelOpenedEvent
+  | SupabaseUiCommandCopiedEvent
path: packages/common/telemetry-constants.ts
user: login: pamelachia
body: ```suggestion
  | SupabaseUiCommandCopyButtonClickedEvent
```
created_at: 2025-03-31T13:24:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-31T11:00:37Z
body: [vc]: #m3sJm5VXqUQG8vqPQjBOoqIi/iPK6p6EBV9YjC4A9mw=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ6b25lLXd3dy1kb3QtY29tIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvd3d3IiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InpvbmUtd3d3LWRvdC1jb20tZ2l0LWNob3JlLXRlbGVtZXRyeS11aS1saWItc3VwYWJhc2UudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2Uvem9uZS13d3ctZG90LWNvbS9DZ1hUbmZ4SkNEeXJOcW5laFFxR3dvODdRaDhiIiwicHJldmlld1VybCI6InpvbmUtd3d3LWRvdC1jb20tZ2l0LWNob3JlLXRlbGVtZXRyeS11aS1saWItc3VwYWJhc2UudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9LHsibmFtZSI6ImRvY3MiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9kb2NzIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2UvZG9jcy9EVjRNazlnSGJBM2ZHdFZBclVtdjZzcFlGWmJUIiwicHJldmlld1VybCI6IiIsIm5leHRDb21taXRTdGF0dXMiOiJDQU5DRUxFRCJ9LHsibmFtZSI6InN0dWRpby1zZWxmLWhvc3RlZCIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3N0dWRpbyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zdXBhYmFzZS9zdHVkaW8tc2VsZi1ob3N0ZWQvRmk1NmpGTHhtbzlzR1FSUmVNVVREazg1VDJ0TCIsInByZXZpZXdVcmwiOiJzdHVkaW8tc2VsZi1ob3N0ZWQtZ2l0LWNob3JlLXRlbGVtZXRyeS11aS1saWItc3VwYWJhc2UudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJzdHVkaW8tc2VsZi1ob3N0ZWQtZ2l0LWNob3JlLXRlbGVtZXRyeS11aS1saWItc3VwYWJhc2UudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJzdHVkaW8iLCJyb290RGlyZWN0b3J5IjoiYXBwcy9zdHVkaW8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2Uvc3R1ZGlvLzNtU0tHZmd5OVpDNjRBTTZ2dFAxaTZleFc2ejYiLCJwcmV2aWV3VXJsIjoiIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19LHsibmFtZSI6InN0dWRpby1zdGFnaW5nIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvc3R1ZGlvIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InN0dWRpby1zdGFnaW5nLWdpdC1jaG9yZS10ZWxlbWV0cnktdWktbGliLXN1cGFiYXNlLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3N1cGFiYXNlL3N0dWRpby1zdGFnaW5nL0V1aUJ3TXFrSGtrTXFFUXlhZ3VTdnlXemtrcXMiLCJwcmV2aWV3VXJsIjoic3R1ZGlvLXN0YWdpbmctZ2l0LWNob3JlLXRlbGVtZXRyeS11aS1saWItc3VwYWJhc2UudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9LHsibmFtZSI6ImRlc2lnbi1zeXN0ZW0iLCJyb290RGlyZWN0b3J5IjoiYXBwcy9kZXNpZ24tc3lzdGVtIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc3VwYWJhc2UvZGVzaWduLXN5c3RlbS9Ba1lhV2FTckRYOExYdkVTV0hKdzh3TXN1WFZzIiwicHJldmlld1VybCI6IiIsIm5leHRDb21taXRTdGF0dXMiOiJDQU5DRUxFRCJ9LHsibmFtZSI6InVpLWxpYnJhcnkiLCJyb290RGlyZWN0b3J5IjoiYXBwcy91aS1saWJyYXJ5IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3N1cGFiYXNlL3VpLWxpYnJhcnkvQ25qdGdiR2FYUzVqWVM0QlpTYllUY0FxUzlGOCIsInByZXZpZXdVcmwiOiJ1aS1saWJyYXJ5LWdpdC1jaG9yZS10ZWxlbWV0cnktdWktbGliLXN1cGFiYXNlLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoidWktbGlicmFyeS1naXQtY2hvcmUtdGVsZW1ldHJ5LXVpLWxpYi1zdXBhYmFzZS52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **design-system** | 🛑 Canceled ([Inspect](https://vercel.com/supabase/design-system/AkYaWaSrDX8LXvESWHJw8wMsuXVs)) |  |  | Mar 31, 2025 1:50pm |
| **docs** | 🛑 Canceled ([Inspect](https://vercel.com/supabase/docs/DV4Mk9gHbA3fGtVArUmv6spYFZbT)) |  |  | Mar 31, 2025 1:50pm |
| **studio-self-hosted** | ✅ Ready ([Inspect](https://vercel.com/supabase/studio-self-hosted/Fi56jFLxmo9sGQRReMUTDk85T2tL)) | [Visit Preview](https://studio-self-hosted-git-chore-telemetry-ui-lib-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/studio-self-hosted-git-chore-telemetry-ui-lib-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 31, 2025 1:50pm |
| **studio-staging** | ✅ Ready ([Inspect](https://vercel.com/supabase/studio-staging/EuiBwMqkHkkMqEQyaguSvyWzkkqs)) | [Visit Preview](https://studio-staging-git-chore-telemetry-ui-lib-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/studio-staging-git-chore-telemetry-ui-lib-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 31, 2025 1:50pm |
| **ui-library** | ✅ Ready ([Inspect](https://vercel.com/supabase/ui-library/CnjtgbGaXS5jYS4BZSbYTcAqS9F8)) | [Visit Preview](https://ui-library-git-chore-telemetry-ui-lib-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/ui-library-git-chore-telemetry-ui-lib-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 31, 2025 1:50pm |
| **zone-www-dot-com** | ✅ Ready ([Inspect](https://vercel.com/supabase/zone-www-dot-com/CgXTnfxJCDyrNqnehQqGwo87Qh8b)) | [Visit Preview](https://zone-www-dot-com-git-chore-telemetry-ui-lib-supabase.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/zone-www-dot-com-git-chore-telemetry-ui-lib-supabase.vercel.app?via=pr-comment-feedback-link) | Mar 31, 2025 1:50pm |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **studio** | ⬜️ Ignored ([Inspect](https://vercel.com/supabase/studio/3mSKGfgy9ZC64AM6vtP1i6exW6z6)) |  |  | Mar 31, 2025 1:50pm |
</details>








author_association: NONE
user: login: supabase[bot]
created_at: 2025-03-31T11:00:39Z
body: [supa]:xguihxuzqibwxjnimxev
This pull request has been ignored for the connected project `xguihxuzqibwxjnimxev` because there are no changes detected in `supabase` directory. You can change this behaviour in [Project Integrations Settings ↗︎](https://supabase.com/dashboard/project/xguihxuzqibwxjnimxev/settings/integrations).

<hr/>

Preview Branches by Supabase.
Learn more about [Supabase Branching ↗︎](https://supabase.com/docs/guides/platform/branching).
author_association: NONE