diff --git a/components/Card.tsx b/components/Card.tsx index 226383e9f..8836b3f97 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -1,5 +1,6 @@ import { ArrowRightIcon } from '@100mslive/react-icons'; import { Flex, Box, Text } from '@100mslive/react-ui'; +import { AppAnalytics } from '../lib/publishEvents'; interface CardProps { icon: any; @@ -17,7 +18,7 @@ const Card: React.FC = ({ icon, title, link, subText, id, cta = 'Read justify="between" onClick={() => { if (link) { - window.analytics.track('card.clicked', { + AppAnalytics.track('card.clicked', { title, link, currentPage: window.location.href diff --git a/components/ChipDropDown.tsx b/components/ChipDropDown.tsx index cc2405df0..613aa786c 100644 --- a/components/ChipDropDown.tsx +++ b/components/ChipDropDown.tsx @@ -6,6 +6,7 @@ import useClickOutside from '@/lib/useClickOutside'; import { getUpdatedPlatformName } from '@/lib/utils'; import Chip from './Chip'; import { menuItem } from './Sidebar'; +import { AppAnalytics } from '../lib/publishEvents'; const ChipDropDown = ({ openFilter, @@ -35,7 +36,7 @@ const ChipDropDown = ({ { - window.analytics.track('platform.changed', { + AppAnalytics.track('platform.changed', { title: document.title, referrer: document.referrer, path: window.location.hostname, diff --git a/components/Code.tsx b/components/Code.tsx index 3a1a04415..41c2b8482 100644 --- a/components/Code.tsx +++ b/components/Code.tsx @@ -1,5 +1,6 @@ import React, { PropsWithChildren } from 'react'; import { Box } from '@100mslive/react-ui'; +import { AppAnalytics } from '../lib/publishEvents'; export const CopyIcon = () => ( ( ); -const Code: React.FC> = - ({ children, section, sectionIndex, tab }) => { - const textRef = React.useRef(null); +const Code: React.FC< + PropsWithChildren<{ section?: string; sectionIndex?: number; tab?: string }> +> = ({ children, section, sectionIndex, tab }) => { + const textRef = React.useRef(null); - const copyFunction = () => { - setCopy(true); - // @ts-ignore - navigator.clipboard.writeText(textRef.current.textContent); - setTimeout(() => { - setCopy(false); - }, 2000); + const copyFunction = () => { + setCopy(true); + // @ts-ignore + navigator.clipboard.writeText(textRef.current.textContent); + setTimeout(() => { + setCopy(false); + }, 2000); - window.analytics.track('copy.to.clipboard', { - title: document.title, - referrer: document.referrer, - path: window.location.hostname, - pathname: window.location.pathname, - href: window.location.href, - section, - sectionIndex, - tab - }); - }; - const [copy, setCopy] = React.useState(false); + AppAnalytics.track('copy.to.clipboard', { + title: document.title, + referrer: document.referrer, + path: window.location.hostname, + pathname: window.location.pathname, + href: window.location.href, + section, + sectionIndex, + tab + }); + }; + const [copy, setCopy] = React.useState(false); - return ( -
-                
-                    
-                        {!copy ? (
-                            
-                        ) : (
-                            
-                        )}
-                    
-                    
-                        {children}
-                    
-                    
+    return (
+        
+            
+                
+                    {!copy ? (
+                        
+                    ) : (
+                        
+                    )}
                 
-            
- ); - }; + + {children} + + +
+
+ ); +}; export default Code; diff --git a/components/ExampleCard.tsx b/components/ExampleCard.tsx index 4e691ccf6..7684f80b5 100644 --- a/components/ExampleCard.tsx +++ b/components/ExampleCard.tsx @@ -1,6 +1,8 @@ +import React from 'react'; import * as reactIcons from '@100mslive/react-icons'; import { Box, Flex, HorizontalDivider, Text } from '@100mslive/react-ui'; import { Technologies, technologyIconMap } from './TechnologySelect'; +import { AppAnalytics } from '../lib/publishEvents'; interface Props extends React.ComponentPropsWithoutRef { title: string; @@ -115,34 +117,33 @@ function IconList({ technologies, showIcon }: IconListProps) { {technology} ); - } else { - return ( - - {technologies.map((technology) => { - let Icon; - const iconNameOrPath = technologyIconMap[technology].icon; - if ( - typeof iconNameOrPath === 'string' && - reactIcons[iconNameOrPath] !== undefined - ) { - Icon = reactIcons[iconNameOrPath]; - } else { - Icon = iconNameOrPath; - } - return ( - - - - ); - })} - - ); } + return ( + + {technologies.map((technology) => { + let Icon; + const iconNameOrPath = technologyIconMap[technology].icon; + if ( + typeof iconNameOrPath === 'string' && + reactIcons[iconNameOrPath] !== undefined + ) { + Icon = reactIcons[iconNameOrPath]; + } else { + Icon = iconNameOrPath; + } + return ( + + + + ); + })} + + ); } type TagListProps = { @@ -156,7 +157,7 @@ function TagList({ tags, title }: TagListProps) { {tags.map((tag) => ( { - window.analytics.track('examples.tag.clicked', { + AppAnalytics.track('examples.tag.clicked', { tag, title }); diff --git a/components/Feedback.tsx b/components/Feedback.tsx index 476fa25ba..2624e46d4 100644 --- a/components/Feedback.tsx +++ b/components/Feedback.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Flex, Box, Button, Text } from '@100mslive/react-ui'; import useClickOutside from '@/lib/useClickOutside'; import { currentUser } from '../lib/currentUser'; +import { AppAnalytics } from '../lib/publishEvents'; const emojis = [{ score: 1 }, { score: 2 }, { score: 3 }, { score: 4 }]; @@ -47,11 +48,11 @@ const Feedback = () => { title={getPlaceholder[`title-${id + 1}`]} style={{ position: 'relative', width: '24px', height: '24px' }} key={emoji.score} - role='button' + role="button" onClick={() => { const userDetails = currentUser(); if (showTextBox === false) { - window.analytics.track('docs.feedback.rating', { + AppAnalytics.track('docs.feedback.rating', { title: document.title, referrer: document.referrer, path: window.location.pathname, @@ -59,7 +60,7 @@ const Feedback = () => { timeStamp: new Date().toLocaleString(), customer_id: userDetails?.customer_id, user_id: userDetails?.user_id, - email: userDetails?.email, + email: userDetails?.email }); setFirstSelection(emoji.score); } @@ -121,7 +122,7 @@ const Feedback = () => { }} onClick={() => { const userDetails = currentUser(); - window.analytics.track('docs.feedback.message', { + AppAnalytics.track('docs.feedback.message', { title: document.title, message: message || '', rating: firstSelection, diff --git a/components/Header.tsx b/components/Header.tsx index 0ba030ff0..4853f2519 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import UtmLinkWrapper from './UtmLinkWrapper'; import { useRouter } from 'next/router'; import { CrossIcon, @@ -10,11 +9,13 @@ import { SearchIcon } from '@100mslive/react-icons'; import { Flex, Text, useTheme } from '@100mslive/react-ui'; -import ActiveLink, { ActiveLinkProps } from './ActiveLink'; -import SearchModal from './SearchModal'; import { WebsiteLink, DashboardLink, GitHubLink, DiscordLink, ContactLink } from '@/lib/utils'; import { references } from 'api-references'; import { exposedPlatformNames } from 'common'; +import SearchModal from './SearchModal'; +import ActiveLink, { ActiveLinkProps } from './ActiveLink'; +import UtmLinkWrapper from './UtmLinkWrapper'; +import { AppAnalytics } from '../lib/publishEvents'; import { NavAPIReference } from './NavAPIReference'; interface Props { @@ -112,7 +113,7 @@ const Header: React.FC = ({ target="_blank" rel="noreferrer" onClick={() => - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId: 'logo.clicked', currentPage: window.location.href }) @@ -124,7 +125,7 @@ const Header: React.FC = ({ - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId: 'docs.clicked', currentPage: window.location.href }) @@ -136,7 +137,7 @@ const Header: React.FC = ({ - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId: 'examples.clicked', currentPage: window.location.href }) @@ -171,7 +172,7 @@ const Header: React.FC = ({ noHighlight target="_blank" onClick={() => - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId: '100ms.live.clicked', currentPage: window.location.href }) @@ -185,7 +186,7 @@ const Header: React.FC = ({ noHighlight target="_blank" onClick={() => - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId: 'sales.clicked', currentPage: window.location.href }) @@ -198,7 +199,7 @@ const Header: React.FC = ({ noHighlight target="_blank" onClick={() => - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId: 'dashboard.clicked', currentPage: window.location.href }) @@ -212,7 +213,7 @@ const Header: React.FC = ({ target="_blank" rel="noreferrer" onClick={() => - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId: 'discord.clicked', currentPage: window.location.href }) @@ -231,7 +232,7 @@ const Header: React.FC = ({ target="_blank" rel="noreferrer" onClick={() => - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId: 'github.clicked', currentPage: window.location.href }) @@ -279,43 +280,41 @@ const HeaderLink = ({ children, noHighlight, ...rest -}: React.PropsWithChildren>) => { - return ( - - {(className) => ( - - {children} - - )} - - ); -}; +}: React.PropsWithChildren>) => ( + + {(className) => ( + + {children} + + )} + +); diff --git a/components/MDXComponents.tsx b/components/MDXComponents.tsx index afedc8d74..0d04eafc8 100644 --- a/components/MDXComponents.tsx +++ b/components/MDXComponents.tsx @@ -27,6 +27,7 @@ import { PortraitImage } from './PortraitImage'; import { CollapsibleRoot, CollapsiblePreview, CollapsibleContent } from './CollapsibleSection'; import { CollapsibleStep } from './CollapsibleStep'; import SuggestedBlogs from './SuggestedBlogs'; +import { AppAnalytics } from '@/lib/publishEvents'; const CodeCustom = (props: any) => {props.children}; @@ -71,7 +72,7 @@ const LinkCustom = (props) => { rel="noopener noreferrer" href={href} onClick={() => - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId, componentId: window?.location?.pathname.split('/')?.[2], // splitArr = ['', 'docs', 'sdk'] page: window?.location?.pathname diff --git a/components/SearchModal.tsx b/components/SearchModal.tsx index d090d8cf9..1caa05f0b 100644 --- a/components/SearchModal.tsx +++ b/components/SearchModal.tsx @@ -1,15 +1,16 @@ import React, { useEffect, useRef, useState } from 'react'; import Image from 'next/image'; -import UtmLinkWrapper from './UtmLinkWrapper'; import { SearchIcon, ArrowRightIcon } from '@100mslive/react-icons'; import { Flex, Box, Text } from '@100mslive/react-ui'; import useClickOutside from '@/lib/useClickOutside'; import algoliasearch from 'algoliasearch/lite'; import { InstantSearch, connectHits, connectSearchBox, Configure } from 'react-instantsearch-dom'; +import { titleCasing } from '@/lib/utils'; import Tag from './Tag'; +import UtmLinkWrapper from './UtmLinkWrapper'; import Chip from './Chip'; import ChipDropDown from './ChipDropDown'; -import { titleCasing } from '@/lib/utils'; +import { AppAnalytics } from '../lib/publishEvents'; const searchClient = algoliasearch( process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || '', @@ -25,14 +26,25 @@ const searchInfoItems = [ { title: 'to navigate', content: [ - , - + , + ] }, { title: 'to select', content: [ { if (hits.length === 0) { - window.analytics.track('no.results', { + AppAnalytics.track('no.results', { title: document.title, referrer: document.referrer, path: window.location.hostname, @@ -204,7 +216,7 @@ const ResultBox = ({ borderRadius: '$0' }} onClick={() => { - window.analytics.track('docs.search.result.clicked', { + AppAnalytics.track('docs.search.result.clicked', { totalNumberOfResults: hits?.length, textInSearch: searchTerm || '', rankOfSearchResult: i + 1, @@ -412,7 +424,7 @@ const SearchModal: React.FC = ({ setModal }) => { }, [hitsCount, searchTerm]); useClickOutside(ref, () => { - window.analytics.track('docs.search.dismissed', { + AppAnalytics.track('docs.search.dismissed', { textInSearch: searchTerm || '', totalNumberOfResults: hitsCount, referrer: document.referrer, @@ -515,7 +527,7 @@ const FilterBar = ({ onClick={() => { if (typeFilter === type) setTypeFilter(ALL_TYPES); else { - window.analytics.track('type.changed', { + AppAnalytics.track('type.changed', { title: document.title, referrer: document.referrer, path: window.location.hostname, @@ -540,8 +552,7 @@ const FilterBar = ({ ); -const getFilterQuery = (platformFilter, typeFilter) => { - return `${platformFilter === ALL_PLATFORMS ? 'NOT ' : ''}platformName:"${platformFilter}" AND ${ +const getFilterQuery = (platformFilter, typeFilter) => + `${platformFilter === ALL_PLATFORMS ? 'NOT ' : ''}platformName:"${platformFilter}" AND ${ typeFilter === ALL_TYPES ? 'NOT ' : '' }type:"${typeFilter}"`; -}; diff --git a/components/SegmentAnalytics.tsx b/components/SegmentAnalytics.tsx index 38a09b6ab..51e34a164 100644 --- a/components/SegmentAnalytics.tsx +++ b/components/SegmentAnalytics.tsx @@ -1,5 +1,5 @@ import React from 'react'; - +import { AppAnalytics } from '../lib/publishEvents'; const SegmentAnalytics = ({ title, options }) => { React.useEffect(() => { if (typeof window !== 'undefined') { @@ -12,7 +12,7 @@ const SegmentAnalytics = ({ title, options }) => { }, {}); // @ts-ignore const url = new URL(window.location.href); - window.analytics.page(title, { + AppAnalytics.page(title, { ...params, ...options, title, @@ -26,7 +26,7 @@ const SegmentAnalytics = ({ title, options }) => { utm_keyword: url.searchParams.get('utm_keyword'), utm_term: url.searchParams.get('utm_term') }); - window.analytics.track('page.viewed', { + AppAnalytics.track('page.viewed', { ...params, ...options, title: document.title, diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index bde4ddcf4..5865fab48 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -1,7 +1,6 @@ /* eslint-disable react/no-array-index-key */ import React, { useEffect, useState, useRef } from 'react'; import { useRouter } from 'next/router'; -import UtmLinkWrapper from './UtmLinkWrapper'; import FlutterIcon from '@/assets/FlutterIcon'; import AndroidIcon from '@/assets/icons/AndroidIcon'; import IosIcon from '@/assets/icons/IosIcon'; @@ -26,9 +25,11 @@ import { import { Listbox } from '@headlessui/react'; import { Flex, Box, Text, CSS } from '@100mslive/react-ui'; import { getUpdatedPlatformName } from '@/lib/utils'; +import { AppAnalytics} from "../publishEvents" import SidebarSection from './SidebarSection'; import ReleaseNotes from './ReleaseNotes'; import PlatformAccordion from './PlatformAccordion'; +import UtmLinkWrapper from './UtmLinkWrapper'; const accordionIconStyle = { height: '24px', width: '24px', color: 'inherit' }; @@ -141,7 +142,7 @@ const Sidebar: React.FC = ({ const changeTech = (s) => { setTech((prevSelection) => { - window.analytics.track('link.clicked', { + AppAnalytics.track('link.clicked', { btnId: 'platform.switched', switchedTo: s.name, switchedFrom: prevSelection.name, @@ -334,7 +335,7 @@ const Sidebar: React.FC = ({ }} onClick={() => { setShowBaseView(true); - window.analytics.track('btn.clicked', { + AppAnalytics.track('btn.clicked', { btnId: 'content.overview.clicked', currentPage: window.location.href }); diff --git a/components/SuggestedBlogs.tsx b/components/SuggestedBlogs.tsx index 03fd0754d..8df674dd1 100644 --- a/components/SuggestedBlogs.tsx +++ b/components/SuggestedBlogs.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { Box, Text } from '@100mslive/react-ui'; import { ExternalLinkIcon } from '@100mslive/react-icons'; +import { AppAnalytics } from '../lib/publishEvents'; + interface Props { suggestedBlogs: Array<{ title: string; @@ -54,7 +56,7 @@ const SuggestedBlogs: React.FC = ({ suggestedBlogs }) => { } }} onClick={() => { - window.analytics.track('docs.blog.redirect', { + AppAnalytics.track('docs.blog.redirect', { type: 'blog_redirect', blog_title: blog.title, path: window.location.pathname, diff --git a/lib/publishEvents.ts b/lib/publishEvents.ts new file mode 100644 index 000000000..550171e4f --- /dev/null +++ b/lib/publishEvents.ts @@ -0,0 +1,154 @@ +import * as amplitude from '@amplitude/analytics-browser'; +import { getUtmParams } from './getUtmParams'; +import { currentUser } from './currentUser'; + +const getCommonOptions = () => ({ + dashboard_version: process.env.REACT_APP_DASHBOARD_VERSION, + events_protocol: process.env.REACT_APP_EVENTS_PROTOCOL, + timestamp: new Date().toString(), + platform: '100ms-docs', + ...getUtmParams() +}); + +// page analytics + +const hubspotPageView = () => { + const path = window.location.pathname; + // eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle, no-multi-assign + const _hsq = (window._hsq = window._hsq || []); + if (_hsq) { + _hsq.push(['setPath', path]); + _hsq.push(['trackPageView']); + } +}; + +// identify analytics +const hubspotIdentify = ({ properties }: { properties: {} }) => { + // eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle, no-multi-assign + const _hsq = (window._hsq = window._hsq || []); + if (_hsq) { + _hsq.push(['identify', { properties }]); + } +}; + +export const analyticsStore: { + data: { workspaceOwnerEmail: null }; + set: (payload: {}) => void; + get: () => {}; +} = { + data: { workspaceOwnerEmail: null }, + set: (payload) => { + for (const index in payload) { + if (Object.prototype.hasOwnProperty.call(payload, index)) { + analyticsStore.data[index] = payload[index]; + } + } + }, + get: () => analyticsStore?.data +}; + +const analyticsTrack = (title, options) => { + try { + const user = currentUser(); + if (!user) { + amplitude.track({ + event_type: title, + event_properties: { + ...getCommonOptions(), + ...options + } + }); + } else if (user && !user.is_admin) { + amplitude.track({ + event_type: title, + event_properties: { + email: user.email, + customer_id: user.customer_id, + + workspaceOwnerEmail: (analyticsStore.get() as { workspaceOwnerEmail: string }) + ?.workspaceOwnerEmail, + api_version: user.api_version, + ...getCommonOptions(), + ...options + } + }); + } + } catch (e) { + console.error(e); + } +}; + +const analyticsPage = (title, options) => { + const user = currentUser(); + if (!user) { + try { + hubspotPageView(); + } catch (e) { + console.error(e); + } + try { + window.analytics.page(title, { + ...getCommonOptions(), + ...options + }); + } catch (e) { + console.error(e); + } + } else if (user && !user.is_admin) { + try { + window.analytics.page(title, { + email: user.email, + customer_id: user.customer_id, + api_version: user.api_version, + ...getCommonOptions(), + ...options + }); + } catch (e) { + console.error(e); + } + try { + hubspotPageView(); + } catch (e) { + console.error(e); + } + } +}; + +const amplitudeIdentify = (userId, properties = {}) => { + amplitude.setUserId(userId); + const identifyEvent = new amplitude.Identify(); + for (const key in properties) { + if (Object.prototype.hasOwnProperty.call(properties, key)) { + identifyEvent.set(key, properties[key]); + } + } + amplitude.identify(identifyEvent); +}; + +const analyticsIdentify = (id, options) => { + const user = currentUser(); + if (!user || (user && !user.is_admin)) { + const finalOptions = { + ...getCommonOptions(), + ...options + }; + try { + hubspotIdentify({ + properties: { ...finalOptions, refId: id, email: user.email, ...user } + }); + } catch (e) { + console.error(e); + } + try { + amplitudeIdentify(id, finalOptions); + } catch (e) { + console.error(e); + } + } +}; + +export const AppAnalytics = { + identify: analyticsIdentify, + track: analyticsTrack, + page: analyticsPage +}; diff --git a/package.json b/package.json index ba44669d1..138a7e2af 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "dependencies": { "@100mslive/react-icons": "0.4.1-alpha.0", "@100mslive/react-ui": "0.4.1-alpha.0", + "@amplitude/analytics-browser": "^2.11.6", "@headlessui/react": "^1.4.0", "@radix-ui/react-select": "^1.2.0", "algoliasearch": "^4.14.3", diff --git a/pages/_app.tsx b/pages/_app.tsx index 8c401ec30..ff7e209c6 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -4,6 +4,7 @@ import { DefaultSeo } from 'next-seo'; import dynamic from 'next/dynamic'; import NProgress from 'nprogress'; import FallbackLayout from '@/layouts/FallbackLayout'; +import * as amplitude from '@amplitude/analytics-browser'; import SEO from '../next-seo.config'; import { currentUser } from '../lib/currentUser'; import '@/styles/custom-ch.css'; @@ -11,11 +12,14 @@ import '@/styles/utils.css'; import '@/styles/nprogress.css'; import '@/styles/theme.css'; import 'inter-ui/inter.css'; +import { AppAnalytics } from '../lib/publishEvents'; declare global { interface Window { // eslint-disable-next-line @typescript-eslint/no-explicit-any analytics: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + _hsq: any; } } @@ -29,8 +33,13 @@ const Application = ({ Component, pageProps }) => { const userDetails = currentUser(); const [count, setCount] = useState(0); React.useEffect(() => { + amplitude.init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY || '', { + autocapture: { + pageViews: true + } + }); if (!!userDetails && Object.keys(userDetails).length !== 0 && count === 0) { - window.analytics.identify(userDetails.customer_id); + AppAnalytics.identify(userDetails.customer_id); setCount(count + 1); } }, [userDetails]); diff --git a/pages/_document.tsx b/pages/_document.tsx index e92467fa4..58cbb0352 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -39,24 +39,11 @@ class MyDocument extends Document { /> {/* To Avoid Flickering */}