diff --git a/apps/docs/lib/constants.ts b/apps/docs/lib/constants.ts index d1bbedbf47c..42c12c0d3f4 100644 --- a/apps/docs/lib/constants.ts +++ b/apps/docs/lib/constants.ts @@ -1,2 +1,3 @@ export const IS_PLATFORM = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' export const LOCAL_SUPABASE = process.env.NEXT_PUBLIC_LOCAL_SUPABASE === 'true' +export const API_URL = process.env.NEXT_PUBLIC_API_URL diff --git a/apps/docs/lib/fetchWrappers.tsx b/apps/docs/lib/fetchWrappers.tsx index c573a0d37dc..0313e49565f 100644 --- a/apps/docs/lib/fetchWrappers.tsx +++ b/apps/docs/lib/fetchWrappers.tsx @@ -1,7 +1,5 @@ interface DataProps { - referrer?: string - title: string - route?: string + [prop: string]: any } export const post = (url: string, data: DataProps, options = {}) => { diff --git a/apps/docs/package.json b/apps/docs/package.json index 0eb29301d62..7f8438339e1 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -56,6 +56,7 @@ "@supabase/supabase-js": "^2.13.0", "algoliasearch": "^4.14.2", "babel": "^6.23.0", + "browser-session-tabs": "^3.0.0", "clsx": "^1.2.1", "common": "*", "config": "*", diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx index 8563418f9a8..e0ad82b5602 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -1,6 +1,6 @@ import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' import { SessionContextProvider } from '@supabase/auth-helpers-react' -import { AuthProvider, ThemeProvider } from 'common' +import { AuthProvider, ThemeProvider, useGoogleAnalyticsProps } from 'common' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import ReactMarkdown from 'react-markdown' @@ -10,8 +10,10 @@ import { CommandMenuProvider } from 'ui' import components from '~/components' import Favicons from '~/components/Favicons' import SiteLayout from '~/layouts/SiteLayout' -import { IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants' +import { API_URL, IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants' import { post } from '~/lib/fetchWrappers' +import { BrowserTabTracker } from 'browser-session-tabs' +import { v4 as uuidv4 } from 'uuid' import '../styles/ch.scss' import '../styles/main.scss?v=1.0.0' import '../styles/new-docs.scss' @@ -19,19 +21,33 @@ import '../styles/prism-okaidia.scss' function MyApp({ Component, pageProps }: AppPropsWithLayout) { const router = useRouter() + const googleAnalyticsProps = useGoogleAnalyticsProps() const [supabase] = useState(() => IS_PLATFORM || LOCAL_SUPABASE ? createBrowserSupabaseClient() : undefined ) function handlePageTelemetry(route: string) { - return post(`https://api.supabase.io/platform/telemetry/page`, { + return post(`${API_URL}/telemetry/page`, { referrer: document.referrer, title: document.title, route, + ga: { + screen_resolution: googleAnalyticsProps?.screenResolution, + language: googleAnalyticsProps?.language, + session_id: BrowserTabTracker.sessionId, + }, }) } + useEffect(() => { + // Generate browser session id for anon tracking + BrowserTabTracker.initialize({ + storageKey: 'supabase.browser.session', + sessionIdGenerator: () => uuidv4(), + }) + }, []) + useEffect(() => { function handleRouteChange(url: string) { /* diff --git a/apps/www/components/Features/index.tsx b/apps/www/components/Features/index.tsx index b5c8dc3f68f..4b291e78767 100644 --- a/apps/www/components/Features/index.tsx +++ b/apps/www/components/Features/index.tsx @@ -1,12 +1,52 @@ -import { Button, Badge, IconArrowRight } from 'ui' -import SectionHeader from 'components/UI/SectionHeader' +import { Badge } from 'ui' import Solutions from 'data/Solutions.json' -import Link from 'next/link' +import Telemetry from '~/lib/telemetry' +import gaEvents from '~/lib/gaEvents' import SectionContainer from '../Layouts/SectionContainer' import ProductIcon from '../ProductIcon' import TextLink from '../TextLink' +import { useGoogleAnalyticsProps } from 'common/hooks/useGoogleAnalyticsProps' +import { useRouter } from 'next/router' const Features = () => { + const router = useRouter() + const googleAnalyticsProps = useGoogleAnalyticsProps() + + const sendTelemetryEvent = async (product: any) => { + switch (product) { + case 'Database': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_database'], + googleAnalyticsProps, + router + ) + case 'Authentication': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_auth'], + googleAnalyticsProps, + router + ) + case 'Storage': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_storage'], + googleAnalyticsProps, + router + ) + case 'Edge Functions': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_edgeFunctions'], + googleAnalyticsProps, + router + ) + case 'Realtime': + return await Telemetry.sendEvent( + gaEvents['www_hp_subhero_products_realtime'], + googleAnalyticsProps, + router + ) + } + } + const IconSections = Object.values(Solutions).map((solution: any) => { const { name, description, icon, label, url } = solution if (solution.name === 'Realtime') return null @@ -24,7 +64,13 @@ const Features = () => { {label} )} - {url && } + {url && ( + sendTelemetryEvent(name)} + /> + )} ) }) diff --git a/apps/www/components/Hero.tsx b/apps/www/components/Hero.tsx index e97cd31fa74..33a1e3deec6 100644 --- a/apps/www/components/Hero.tsx +++ b/apps/www/components/Hero.tsx @@ -1,10 +1,19 @@ -import { Button, IconBookOpen, Space } from 'ui' -import Link from 'next/link' +import { Button, IconBookOpen } from 'ui' import { useRouter } from 'next/router' +import Link from 'next/link' import SectionContainer from './Layouts/SectionContainer' +import Telemetry, { GoogleAnalyticsEvent } from '~/lib/telemetry' +import gaEvents from '~/lib/gaEvents' +import { useGoogleAnalyticsProps } from 'common/hooks/useGoogleAnalyticsProps' const Hero = () => { - const { basePath } = useRouter() + const router = useRouter() + const { basePath } = router + const googleAnalyticsProps = useGoogleAnalyticsProps() + + const sendTelemetryEvent = async (event: GoogleAnalyticsEvent) => { + await Telemetry.sendEvent(event, googleAnalyticsProps, router) + } return (
@@ -40,14 +49,16 @@ const Hero = () => {
- + sendTelemetryEvent(gaEvents['www_hp_hero_startProject'])}> - + sendTelemetryEvent(gaEvents['www_hp_hero_documentation'])} + > diff --git a/apps/www/components/TextLink.tsx b/apps/www/components/TextLink.tsx index 3bd3edfc10e..73d54f6136e 100644 --- a/apps/www/components/TextLink.tsx +++ b/apps/www/components/TextLink.tsx @@ -1,20 +1,22 @@ -import { IconArrowRight, IconChevronRight } from 'ui' +import { IconChevronRight } from 'ui' import Link from 'next/link' -function TextLink({ url, label }: any) { +function TextLink({ url = '', label, ...props }: any) { return ( - -
- {`${label} about ${url}`} - {label} -
- + + +
+ {`${label} about ${url}`} + {label} +
+ +
-
- + + ) } diff --git a/apps/www/data/Solutions.json b/apps/www/data/Solutions.json index f12ff0f89fa..823d5dd261d 100644 --- a/apps/www/data/Solutions.json +++ b/apps/www/data/Solutions.json @@ -23,14 +23,6 @@ "label": "", "url": "/storage" }, - "realtime": { - "name": "Realtime", - "icon": "M15.042 21.672L13.684 16.6m0 0l-2.51 2.225.569-9.47 5.227 7.917-3.286-.672zM12 2.25V4.5m5.834.166l-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243l-1.59-1.59", - "description": "Store, organize, and serve large files. Any media, including videos and images.", - "description_short": "", - "label": "", - "url": "/storage" - }, "edge-functions": { "name": "Edge Functions", "icon": "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4", diff --git a/apps/www/lib/constants.ts b/apps/www/lib/constants.ts index e3c815b5dca..445c0a6810a 100644 --- a/apps/www/lib/constants.ts +++ b/apps/www/lib/constants.ts @@ -1,5 +1,8 @@ export const APP_NAME = 'Supabase' export const DESCRIPTION = 'The Open Source Alternative to Firebase.' +export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' +export const IS_PREVIEW = process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' +export const API_URL = process.env.NEXT_PUBLIC_API_URL // Launch Weeek export const SAMPLE_TICKET_NUMBER = 1234 diff --git a/apps/www/lib/fetchWrapper.ts b/apps/www/lib/fetchWrapper.ts index c573a0d37dc..0313e49565f 100644 --- a/apps/www/lib/fetchWrapper.ts +++ b/apps/www/lib/fetchWrapper.ts @@ -1,7 +1,5 @@ interface DataProps { - referrer?: string - title: string - route?: string + [prop: string]: any } export const post = (url: string, data: DataProps, options = {}) => { diff --git a/apps/www/lib/gaEvents.ts b/apps/www/lib/gaEvents.ts new file mode 100644 index 00000000000..73599720f86 --- /dev/null +++ b/apps/www/lib/gaEvents.ts @@ -0,0 +1,45 @@ +import { GoogleAnalyticsEvent } from './telemetry' + +interface GoogleAnalyticsEventType { + [key: string]: GoogleAnalyticsEvent +} + +const GoogleAnalyticsEvents: GoogleAnalyticsEventType = { + www_hp_hero_startProject: { + action: 'www_hp_hero_startProject', + category: 'link', + label: '', + }, + www_hp_hero_documentation: { + action: 'www_hp_hero_documentation', + category: 'link', + label: '', + }, + www_hp_subhero_products_database: { + action: 'www_hp_subhero_products_database', + category: 'link', + label: '', + }, + www_hp_subhero_products_auth: { + action: 'www_hp_subhero_products_auth', + category: 'link', + label: '', + }, + www_hp_subhero_products_storage: { + action: 'www_hp_subhero_products_storage', + category: 'link', + label: '', + }, + www_hp_subhero_products_edgeFunctions: { + action: 'www_hp_subhero_products_edgeFunctions', + category: 'link', + label: '', + }, + www_hp_subhero_products_realtime: { + action: 'www_hp_subhero_products_realtime', + category: 'link', + label: '', + }, +} + +export default GoogleAnalyticsEvents diff --git a/apps/www/lib/telemetry.ts b/apps/www/lib/telemetry.ts new file mode 100644 index 00000000000..e64f0992b68 --- /dev/null +++ b/apps/www/lib/telemetry.ts @@ -0,0 +1,48 @@ +import { post } from '~/lib/fetchWrapper' +import { API_URL, IS_PROD, IS_PREVIEW } from 'lib/constants' +import { BrowserTabTracker } from 'browser-session-tabs' +import { NextRouter } from 'next/router' + +export interface GoogleAnalyticsEvent { + category: string + action: string + label: string + value?: string +} + +export interface GoogleAnalyticsProps { + screenResolution?: string + language: string +} + +// This event is the same as in studio/lib/telemetry.tx +// but uses different ENV variables for www + +const sendEvent = ( + event: GoogleAnalyticsEvent, + gaProps: GoogleAnalyticsProps, + router: NextRouter +) => { + if (!IS_PROD && !IS_PREVIEW) return + + const { category, action, label, value } = event + + return post(`${API_URL}/telemetry/event`, { + action: action, + category: category, + label: label, + value: value, + page_referrer: document?.referrer, + page_title: document?.title, + page_location: router.asPath, + ga: { + screen_resolution: gaProps?.screenResolution, + language: gaProps?.language, + session_id: BrowserTabTracker.sessionId, + }, + }) +} + +export default { + sendEvent, +} diff --git a/apps/www/package.json b/apps/www/package.json index cadcf99b872..8242300daea 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -20,6 +20,7 @@ "@next/mdx": "^12.3.2", "@supabase/auth-ui-react": "^0.1.6", "@supabase/supabase-js": "^2.13.0", + "browser-session-tabs": "^3.0.0", "classnames": "^2.3.1", "cobe": "^0.6.2", "common": "*", diff --git a/apps/www/pages/_app.tsx b/apps/www/pages/_app.tsx index 2ccb053b3ae..ab0dbf807d2 100644 --- a/apps/www/pages/_app.tsx +++ b/apps/www/pages/_app.tsx @@ -1,25 +1,41 @@ -import { APP_NAME, DESCRIPTION } from 'lib/constants' +import { API_URL, APP_NAME, DESCRIPTION } from 'lib/constants' import { DefaultSeo } from 'next-seo' import { AppProps } from 'next/app' import { useRouter } from 'next/router' import { useEffect } from 'react' import Meta from '~/components/Favicons' import '../styles/index.css' -import { post } from './../lib/fetchWrapper' -import { AuthProvider, ThemeProvider } from 'common' +import { post } from '~/lib/fetchWrapper' +import { AuthProvider, ThemeProvider, useGoogleAnalyticsProps } from 'common' +import { BrowserTabTracker } from 'browser-session-tabs' +import { v4 as uuidv4 } from 'uuid' import Head from 'next/head' export default function MyApp({ Component, pageProps }: AppProps) { const router = useRouter() + const googleAnalyticsProps = useGoogleAnalyticsProps() function handlePageTelemetry(route: string) { - return post(`https://api.supabase.io/platform/telemetry/page`, { + return post(`${API_URL}/telemetry/page`, { referrer: document.referrer, title: document.title, route, + ga: { + screen_resolution: googleAnalyticsProps?.screenResolution, + language: googleAnalyticsProps?.language, + session_id: BrowserTabTracker.sessionId, + }, }) } + useEffect(() => { + // Generate browser session id for anon tracking + BrowserTabTracker.initialize({ + storageKey: 'supabase.browser.session', + sessionIdGenerator: () => uuidv4(), + }) + }, []) + useEffect(() => { function handleRouteChange(url: string) { handlePageTelemetry(url) diff --git a/package-lock.json b/package-lock.json index 9ed78cb140f..4b3477ab38d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "@supabase/supabase-js": "^2.13.0", "algoliasearch": "^4.14.2", "babel": "^6.23.0", + "browser-session-tabs": "^3.0.0", "clsx": "^1.2.1", "common": "*", "config": "*", @@ -673,6 +674,7 @@ "@next/mdx": "^12.3.2", "@supabase/auth-ui-react": "^0.1.6", "@supabase/supabase-js": "^2.13.0", + "browser-session-tabs": "^3.0.0", "classnames": "^2.3.1", "cobe": "^0.6.2", "common": "*", @@ -12807,6 +12809,22 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, + "node_modules/browser-session-tabs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/browser-session-tabs/-/browser-session-tabs-3.0.0.tgz", + "integrity": "sha512-HBJWkCbcDYFL+hlSGld54SF1SgaiSXnw9vaTPdDeOxT9HCTFt3hVdArIroQ3HDhh9vhqkbl6TkMjTFok0ZfwIA==", + "dependencies": { + "js-cookie": "^3.0.1" + } + }, + "node_modules/browser-session-tabs/node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, "node_modules/browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -39189,6 +39207,7 @@ "analytics-node": "^3.5.0", "awesome-debounce-promise": "^2.1.0", "blueimp-md5": "^2.19.0", + "browser-session-tabs": "^3.0.0", "clipboard": "^2.0.8", "clsx": "^1.2.1", "common": "*", @@ -48910,6 +48929,21 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, + "browser-session-tabs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/browser-session-tabs/-/browser-session-tabs-3.0.0.tgz", + "integrity": "sha512-HBJWkCbcDYFL+hlSGld54SF1SgaiSXnw9vaTPdDeOxT9HCTFt3hVdArIroQ3HDhh9vhqkbl6TkMjTFok0ZfwIA==", + "requires": { + "js-cookie": "^3.0.1" + }, + "dependencies": { + "js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==" + } + } + }, "browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -51538,6 +51572,7 @@ "@types/react": "17.0.39", "algoliasearch": "^4.14.2", "babel": "^6.23.0", + "browser-session-tabs": "^3.0.0", "clsx": "^1.2.1", "common": "*", "config": "*", @@ -66421,6 +66456,7 @@ "aws-sdk": "^2.1354.0", "babel-loader": "^8.2.3", "blueimp-md5": "^2.19.0", + "browser-session-tabs": "^3.0.0", "clipboard": "^2.0.8", "clsx": "^1.2.1", "common": "*", @@ -69873,6 +69909,7 @@ "@types/react-dom": "^17.0.11", "@types/react-syntax-highlighter": "^13.5.2", "autoprefixer": "^10.4.12", + "browser-session-tabs": "^3.0.0", "classnames": "^2.3.1", "cobe": "^0.6.2", "common": "*", diff --git a/packages/common/hooks/index.ts b/packages/common/hooks/index.ts index 7560f7ebcf5..1d07190bbf5 100644 --- a/packages/common/hooks/index.ts +++ b/packages/common/hooks/index.ts @@ -1,2 +1,3 @@ export * from './useBreakpoint' export * from './useParams' +export * from './useGoogleAnalyticsProps' diff --git a/packages/common/hooks/useGoogleAnalyticsProps.ts b/packages/common/hooks/useGoogleAnalyticsProps.ts new file mode 100644 index 00000000000..3b247008f9f --- /dev/null +++ b/packages/common/hooks/useGoogleAnalyticsProps.ts @@ -0,0 +1,12 @@ +import { useRouter } from 'next/router' + +const isBrowser = typeof window !== 'undefined' + +export function useGoogleAnalyticsProps() { + const { locale } = useRouter() + + return { + screenResolution: isBrowser ? `${window.innerWidth}x${window.innerHeight}` : undefined, + language: locale ?? 'en-US', + } +} diff --git a/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx b/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx index 43548b7ec7d..f400dbd6a13 100644 --- a/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx +++ b/studio/components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx @@ -8,7 +8,7 @@ import { useProfileQuery } from 'data/profile/profile-query' import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.constants' import SQLCard from './SQLCard' import { createSqlSnippetSkeleton } from '../SQLEditor.utils' -import { useParams } from 'common' +import { useGoogleAnalyticsProps, useParams } from 'common' import { useRouter } from 'next/router' import { SqlSnippet } from 'data/content/sql-snippets-query' import { useSqlEditorStateSnapshot } from 'state/sql-editor' @@ -21,6 +21,7 @@ const SQLTemplates = observer(() => { const { data: profile } = useProfileQuery() const [sql, quickStart] = partition(SQL_TEMPLATES, { type: 'template' }) + const googleAnalyticsProps = useGoogleAnalyticsProps() const snap = useSqlEditorStateSnapshot() const canCreateSQLSnippet = checkPermissions(PermissionAction.CREATE, 'user_content', { resource: { type: 'sql', owner_id: profile?.id }, @@ -77,7 +78,8 @@ const SQLTemplates = observer(() => { action: 'script_clicked', label: x.title, }, - ui.googleAnalyticsProps + googleAnalyticsProps, + router ) }} /> @@ -111,7 +113,8 @@ const SQLTemplates = observer(() => { action: 'quickstart_clicked', label: x.title, }, - ui.googleAnalyticsProps + googleAnalyticsProps, + router ) }} /> diff --git a/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx b/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx index 2ba8e6a3324..4d47dcedb13 100644 --- a/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx +++ b/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx @@ -9,6 +9,8 @@ import { CSVLink } from 'react-csv' import { useSqlEditorStateSnapshot } from 'state/sql-editor' // @ts-ignore import MarkdownTable from 'markdown-table' +import { useGoogleAnalyticsProps } from 'common' +import { useRouter } from 'next/router' export type ResultsDropdownProps = { id: string @@ -17,9 +19,11 @@ export type ResultsDropdownProps = { const ResultsDropdown = ({ id }: ResultsDropdownProps) => { const { project } = useProjectContext() const snap = useSqlEditorStateSnapshot() + const googleAnalyticsProps = useGoogleAnalyticsProps() const result = snap.results?.[id]?.[0] ?? undefined const { ui } = useStore() const csvRef = useRef(null) + const router = useRouter() const csvData = useMemo( () => (result?.rows ? compact(Array.from(result.rows || [])) : ''), @@ -30,7 +34,8 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => { csvRef.current?.link.click() Telemetry.sendEvent( { category: 'sql_editor', action: 'sql_download_csv', label: '' }, - ui.googleAnalyticsProps + googleAnalyticsProps, + router ) } @@ -53,7 +58,8 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => { ui.setNotification({ category: 'success', message: 'Copied results to clipboard' }) Telemetry.sendEvent( { category: 'sql_editor', action: 'sql_copy_as_markdown', label: '' }, - ui.googleAnalyticsProps + googleAnalyticsProps, + router ) }) } diff --git a/studio/components/ui/PageTelemetry.tsx b/studio/components/ui/PageTelemetry.tsx index 412336e28d0..54b9155cec1 100644 --- a/studio/components/ui/PageTelemetry.tsx +++ b/studio/components/ui/PageTelemetry.tsx @@ -1,4 +1,5 @@ -import { useStore } from 'hooks' +import { BrowserTabTracker } from 'browser-session-tabs' +import { useGoogleAnalyticsProps } from 'common' import { post } from 'lib/common/fetch' import { API_URL, IS_PLATFORM } from 'lib/constants' import { observer } from 'mobx-react-lite' @@ -25,7 +26,7 @@ function sanitizePageViewRoute(_route?: string) { const PageTelemetry: FC = ({ children }) => { const router = useRouter() - const { ui } = useStore() + const googleAnalyticsProps = useGoogleAnalyticsProps() useEffect(() => { function handleRouteChange(url: string) { @@ -71,8 +72,9 @@ const PageTelemetry: FC = ({ children }) => { title: document.title, route, ga: { - screen_resolution: ui.googleAnalyticsProps?.screenResolution, - language: ui.googleAnalyticsProps?.language, + screen_resolution: googleAnalyticsProps?.screenResolution, + language: googleAnalyticsProps?.language, + session_id: BrowserTabTracker.sessionId, }, }) } diff --git a/studio/lib/auth.tsx b/studio/lib/auth.tsx index c7b962a2010..f02f3688130 100644 --- a/studio/lib/auth.tsx +++ b/studio/lib/auth.tsx @@ -1,18 +1,20 @@ import { AuthContext as AuthContextInternal, AuthProvider as AuthProviderInternal, - AuthProviderProps, gotrueClient, + useGoogleAnalyticsProps, } from 'common' import { useProfileQuery } from 'data/profile/profile-query' import { useStore } from 'hooks' import { PropsWithChildren, useEffect } from 'react' import { GOTRUE_ERRORS, IS_PLATFORM } from './constants' +import Telemetry from 'lib/telemetry' export const AuthContext = AuthContextInternal export const AuthProvider = ({ children }: PropsWithChildren<{}>) => { const { ui, app } = useStore() + const googleAnalyticsProps = useGoogleAnalyticsProps() // Check for unverified GitHub users after a GitHub sign in useEffect(() => { @@ -34,7 +36,7 @@ export const AuthProvider = ({ children }: PropsWithChildren<{}>) => { // Track telemetry for the current user useProfileQuery({ onSuccess(profile) { - ui.setProfile(profile) + Telemetry.sendIdentify(profile, googleAnalyticsProps) // [Joshen] Temp fix: For new users, the GET profile call also creates a default org // But because the dashboard's logged in state is using gotrue as the source of truth diff --git a/studio/lib/telemetry.ts b/studio/lib/telemetry.ts index ea5dcb38bcb..f3a75970c6a 100644 --- a/studio/lib/telemetry.ts +++ b/studio/lib/telemetry.ts @@ -1,6 +1,8 @@ import { post } from 'lib/common/fetch' import { API_URL, IS_PLATFORM } from 'lib/constants' import { User } from 'types' +import { BrowserTabTracker } from 'browser-session-tabs' +import { NextRouter } from 'next/router' export interface GoogleAnalyticsProps { screenResolution?: string @@ -14,7 +16,8 @@ const sendEvent = ( label: string value?: string }, - gaProps?: GoogleAnalyticsProps + gaProps: GoogleAnalyticsProps, + router: NextRouter ) => { if (!IS_PLATFORM) return @@ -25,9 +28,13 @@ const sendEvent = ( category: category, label: label, value: value, + page_referrer: document?.referrer, + page_title: document?.title, + page_location: router.asPath, ga: { screen_resolution: gaProps?.screenResolution, language: gaProps?.language, + session_id: BrowserTabTracker.sessionId, }, }) } @@ -44,6 +51,7 @@ const sendIdentify = (user: User, gaProps?: GoogleAnalyticsProps) => { ga: { screen_resolution: gaProps?.screenResolution, language: gaProps?.language, + session_id: BrowserTabTracker.sessionId, }, }) } diff --git a/studio/package.json b/studio/package.json index 2f845d6011e..e85e5682e10 100644 --- a/studio/package.json +++ b/studio/package.json @@ -41,6 +41,7 @@ "analytics-node": "^3.5.0", "awesome-debounce-promise": "^2.1.0", "blueimp-md5": "^2.19.0", + "browser-session-tabs": "^3.0.0", "clipboard": "^2.0.8", "clsx": "^1.2.1", "common": "*", diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 8a9144cf26f..c35c5c382ae 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -50,6 +50,8 @@ import Favicons from 'components/head/Favicons' import { IS_PLATFORM } from 'lib/constants' import { SessionContextProvider } from '@supabase/auth-helpers-react' import { createClient } from '@supabase/supabase-js' +import { BrowserTabTracker } from 'browser-session-tabs' +import { v4 as uuidv4 } from 'uuid' dayjs.extend(customParseFormat) dayjs.extend(utc) @@ -74,6 +76,14 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) { const getSavingState = () => rootStore.content.savingState + useEffect(() => { + // Generate browser session id for anon tracking + BrowserTabTracker.initialize({ + storageKey: 'supabase.browser.session', + sessionIdGenerator: () => uuidv4(), + }) + }, []) + // prompt the user if they try and leave with unsaved content store changes useEffect(() => { const warningText = 'You have unsaved changes - are you sure you wish to leave this page?' diff --git a/studio/stores/UiStore.ts b/studio/stores/UiStore.ts index 1b91898ae31..0e7b35339f9 100644 --- a/studio/stores/UiStore.ts +++ b/studio/stores/UiStore.ts @@ -1,8 +1,7 @@ import { uuidv4 } from 'lib/helpers' import { action, makeAutoObservable } from 'mobx' -import { Project, Notification, User, Organization, ProjectBase, Permission } from 'types' +import { Project, Notification, Organization, ProjectBase, Permission } from 'types' import { IRootStore } from './RootStore' -import Telemetry, { GoogleAnalyticsProps } from 'lib/telemetry' export interface IUiStore { language: 'en-US' @@ -12,12 +11,10 @@ export interface IUiStore { selectedOrganization?: Organization notification?: Notification permissions?: Permission[] - googleAnalyticsProps?: GoogleAnalyticsProps load: () => void setProjectRef: (ref?: string) => void setOrganizationSlug: (slug?: string) => void setNotification: (notification: Notification) => string - setProfile: (value: User) => void setPermissions: (permissions?: Permission[]) => void } export default class UiStore implements IUiStore { @@ -83,14 +80,6 @@ export default class UiStore implements IUiStore { return undefined } - get googleAnalyticsProps() { - return { - screenResolution: - typeof window !== 'undefined' ? `${window.innerWidth}x${window.innerHeight}` : undefined, - language: this.language, - } - } - load() { if (typeof window === 'undefined') return } @@ -109,10 +98,6 @@ export default class UiStore implements IUiStore { return id } - setProfile(value: User) { - Telemetry.sendIdentify(value, this.googleAnalyticsProps) - } - setPermissions(permissions?: any) { this.permissions = permissions }