diff --git a/bin/populate_env b/bin/populate_env index d99de9b6..892eac63 100755 --- a/bin/populate_env +++ b/bin/populate_env @@ -11,11 +11,13 @@ search_enabled="${REACT_APP_SEMANTIC_SEARCH_ENABLED:-true}" search_url="${REACT_APP_HELX_SEARCH_URL}" brand_name="${REACT_APP_UI_BRAND_NAME}" tranql_url="${REACT_APP_TRANQL_URL:-\/tranql}" -analytics="${REACT_APP_ANALYTICS:-}" hidden_support_sections="${REACT_APP_HIDDEN_SUPPORT_SECTIONS}" hidden_result_tabs="${REACT_APP_HIDDEN_RESULT_TABS}" deployment_namespace="${REACT_APP_DEPLOYMENT_NAMESPACE}" appstore_asset_branch="${REACT_APP_APPSTORE_ASSET_BRANCH}" +analytics_enabled="${REACT_APP_ANALYTICS_ENABLED:-false}" +analytics_platform="${REACT_APP_ANALYTICS_PLATFORM}" +analytics_token="${REACT_APP_ANALYTICS_TOKEN}" meta_title="${META_TITLE:-HeLx UI}" meta_description="${META_DESCRIPTION:-HeLx UI}" @@ -28,12 +30,9 @@ template='{ "workspaces_enabled": "%WORKSPACES_ENABLED%", "tranql_url": "%TRANQL_URL%", "analytics": { - "enabled": true, - "platform": "mixpanel", - "auth": { - "mixpanel_token": "%ANALYTICS%", - "ga_property": "" - } + "enabled": %ANALYTICS_ENABLED%, + "platform": "%ANALYTICS_PLATFORM%", + "token": "%ANALYTICS_TOKEN%" }, "hidden_support_sections": "%HIDDEN_SUPPORT_SECTIONS%", "hidden_result_tabs": "%HIDDEN_RESULT_TABS%", @@ -52,10 +51,12 @@ echo "$template" | sed \ -e "s/%BRAND%/$brand_name/" \ -e "s/%HIDDEN_SUPPORT_SECTIONS%/$hidden_support_sections/" \ -e "s/%HIDDEN_RESULT_TABS%/$hidden_result_tabs/" \ - -e "s/%ANALYTICS%/$analytics/" \ -e "s/%TRANQL_URL%/$tranql_url/" \ -e "s/%DEPLOYMENT_NAMESPACE%/$deployment_namespace/" \ -e "s/%APPSTORE_ASSET_BRANCH%/$appstore_asset_branch/" \ + -e "s/%ANALYTICS_ENABLED%/$analytics_enabled/" \ + -e "s/%ANALYTICS_PLATFORM%/$analytics_platform/" \ + -e "s/%ANALYTICS_TOKEN%/$analytics_token/" \ -e "s/%META_TITLE%/$meta_title/" \ -e "s/%META_DESCRIPTION%/$meta_description/" \ > $1 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 536e8705..a77ad695 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "name": "helx-ui", "dependencies": { "@ant-design/plots": "^1.0.9", - "@ant-design/pro-form": "^1.65.0", + "@ant-design/pro-form": "^1.74.7", "@antv/g2plot": "^2.4.8", "@gatsbyjs/reach-router": "^1.3.9", "@testing-library/jest-dom": "^5.11.4", @@ -24,7 +24,7 @@ "elasticlunr": "^0.9.5", "es6-error": "^4.1.1", "exponential-backoff": "^3.1.0", - "helx-analytics": "^1.0.14", + "helx-analytics": "^2.0.0", "js-cookie": "^3.0.1", "lunr": "^2.3.9", "nanoevents": "^6.0.2", @@ -78,7 +78,7 @@ "type-fest": "^3.8.0", "typescript": "^4.7.4", "webpack": "^5.80.0", - "webpack-cli": "^5.0.1", + "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.13.3", "webpack-merge": "^5.8.0" } @@ -8243,15 +8243,14 @@ } }, "node_modules/helx-analytics": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/helx-analytics/-/helx-analytics-1.0.14.tgz", - "integrity": "sha512-TKjgPEFsm4y7ELsv3Ze1JIj/WcTRXfOHDmlyw/Kg71BNVnw5TqVqry/+A0vJZCAOrdkep/y4bKfNCIg1xILDhg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/helx-analytics/-/helx-analytics-2.0.0.tgz", + "integrity": "sha512-9lY6/+e78Zsiwpw9VcCvRNQt7wJZC2N7z2EAF1TKX8dXwWeAEo1mIy92kvHGi5MfRob8o9tj3w2jTXNwPbCDOQ==", "dependencies": { "@babel/plugin-proposal-class-properties": "^7.14.5", "@types/mixpanel-browser": "^2.35.7", "mixpanel-browser": "^2.41.0", - "react": "^17.0.2", - "react-ga": "^3.3.0" + "react-ga4": "^2.1.0" } }, "node_modules/highlight-words-core": { @@ -12852,14 +12851,10 @@ "react": "17.0.2" } }, - "node_modules/react-ga": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-3.3.1.tgz", - "integrity": "sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ==", - "peerDependencies": { - "prop-types": "^15.6.0", - "react": "^15.6.2 || ^16.0 || ^17 || ^18" - } + "node_modules/react-ga4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz", + "integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==" }, "node_modules/react-highlight-words": { "version": "0.18.0", diff --git a/package.json b/package.json index 984865c6..6903c593 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "elasticlunr": "^0.9.5", "es6-error": "^4.1.1", "exponential-backoff": "^3.1.0", - "helx-analytics": "^1.0.14", + "helx-analytics": "^2.0.0", "js-cookie": "^3.0.1", "lunr": "^2.3.9", "nanoevents": "^6.0.2", diff --git a/src/app.js b/src/app.js index 570595f9..4c5aea6a 100644 --- a/src/app.js +++ b/src/app.js @@ -12,15 +12,15 @@ const ContextProviders = ({ children }) => { - - + + {children} - - + + diff --git a/src/components/layout/layout.js b/src/components/layout/layout.js index 59e475a0..330d15c0 100644 --- a/src/components/layout/layout.js +++ b/src/components/layout/layout.js @@ -24,12 +24,11 @@ export const Layout = ({ children }) => { const logout = async () => { setLoggingOut(true) - analyticsEvents.logout() + analyticsEvents.workspacesLogout() try { await api.logout() } catch (e) {} setLoggingOut(false) - // logoutHandler(helxAppstoreUrl) } const removeTrailingSlash = (url) => url.endsWith("/") ? url.slice(0, url.length - 1) : url const activeRoutes = routes.filter((route) => ( @@ -97,11 +96,10 @@ export const Layout = ({ children }) => { context?.brand === 'heal' ? : } - ) } \ No newline at end of file diff --git a/src/components/layout/menu/mobile-menu.js b/src/components/layout/menu/mobile-menu.js index 65f51369..bb3aee39 100644 --- a/src/components/layout/menu/mobile-menu.js +++ b/src/components/layout/menu/mobile-menu.js @@ -1,23 +1,28 @@ import React, { useState } from 'react' import { Button, Drawer, Menu } from 'antd' import { MenuOutlined } from '@ant-design/icons' -import { logoutHandler } from '../../../api/' import { Link } from '@gatsbyjs/reach-router' import { useEnvironment } from '../../../contexts/environment-context' -import { useAnalytics } from '../../../contexts' +import { useAnalytics, useWorkspacesAPI } from '../../../contexts' import '../layout.css' export const MobileMenu = ({menu}) => { const [visible, setVisible] = useState(false); - const { helxAppstoreUrl, context } = useEnvironment(); + const [loggingOut, setLoggingOut] = useState(false) + const { context } = useEnvironment(); + const { api, loggedIn, loading: apiLoading } = useWorkspacesAPI() const { analyticsEvents } = useAnalytics(); const baseLinkPath = context.workspaces_enabled === 'true' ? '/helx' : '' - const logout = () => { - analyticsEvents.logout(); - logoutHandler(helxAppstoreUrl); - } + const logout = async () => { + setLoggingOut(true) + analyticsEvents.workspacesLogout() + try { + await api.logout() + } catch (e) {} + setLoggingOut(false) + } return (
@@ -34,8 +39,13 @@ export const MobileMenu = ({menu}) => { visible={visible} > - {menu.map(m => m['text'] !== '' && {m.text})} - + { + menu.map(m => m['text'] !== '' && ( + {m.text} + )) } + { context.workspaces_enabled === 'true' && !apiLoading && loggedIn && ( + + ) }
diff --git a/src/components/search/concept-card/studies-tab.js b/src/components/search/concept-card/studies-tab.js index bef27899..4c09ec89 100644 --- a/src/components/search/concept-card/studies-tab.js +++ b/src/components/search/concept-card/studies-tab.js @@ -1,11 +1,13 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react' import { List, Spin, Space, Tag, Typography, Divider } from 'antd' +import { useAnalytics } from '../../../contexts' import { Link } from '../../link' const { Text } = Typography const { CheckableTag: CheckableFacet } = Tag export const StudiesTab = ({ studies }) => { + const { analyticsEvents } = useAnalytics() const [facets, setFacets] = useState([]) const [selectedFacets, setSelectedFacets] = useState([]) @@ -28,6 +30,10 @@ export const StudiesTab = ({ studies }) => { setSelectedFacets([...newSelection]) }, [selectedFacets]) + const studyLinkClicked = (studyId) => { + analyticsEvents.studyLinkClicked(studyId) + } + useEffect(() => { const facets = (studies ?? []).reduce((acc, study) => { if (!acc.includes(study.type)) acc.push(study.type) @@ -93,7 +99,7 @@ export const StudiesTab = ({ studies }) => { { (study.c_id && study.c_link) && ( { ` ` } - ({ study.c_id }) + ( studyLinkClicked(study.c_id) }>{ study.c_id }) )} diff --git a/src/components/search/concept-modal/concept-modal.js b/src/components/search/concept-modal/concept-modal.js index 5b58a108..c9e50e35 100644 --- a/src/components/search/concept-modal/concept-modal.js +++ b/src/components/search/concept-modal/concept-modal.js @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Button, Menu, Modal, Result, Space, Spin, Typography, Tooltip, Divider } from 'antd' import CustomIcon, { InfoCircleOutlined as OverviewIcon, @@ -113,19 +113,21 @@ export const ConceptModalBody = ({ result }) => { delete links[tab] }) - const setCurrentTab = (() => { + const setCurrentTab = useCallback((() => { let oldTime = Date.now(); return (tabName) => { const newTime = Date.now(); const elapsed = newTime - oldTime; - if (tabName !== currentTab) { - // Make sure we only track events when the tab actually changes. - analyticsEvents.resultTabSelected(tabs[tabName].title, tabs[currentTab].title, elapsed) - } + _setCurrentTab((currentTab) => { + if (tabName !== currentTab) { + // Make sure we only track events when the tab actually changes. + analyticsEvents.resultTabSelected(tabs[tabName].title, tabs[currentTab].title, elapsed) + } + return tabName + }) oldTime = newTime; - _setCurrentTab(tabName); } - })() + })(), [tabs]) useEffect(() => { setCurrentTab('overview') diff --git a/src/components/search/concept-modal/tabs/cdes/cde-item.js b/src/components/search/concept-modal/tabs/cdes/cde-item.js index 13d96cbe..ee7c2719 100644 --- a/src/components/search/concept-modal/tabs/cdes/cde-item.js +++ b/src/components/search/concept-modal/tabs/cdes/cde-item.js @@ -4,6 +4,7 @@ import { ExportOutlined } from '@ant-design/icons' import _Highlighter from 'react-highlight-words' import { RelatedConceptsList } from './related-concepts' import { SideCollapse } from '../../../..' +import { useAnalytics } from '../../../../../contexts' const { Text, Link } = Typography const { Panel } = Collapse @@ -20,6 +21,8 @@ const Section = ({ title, children }) => ( export const CdeItem = ({ cde, cdeRelatedConcepts, highlight }) => { const [collapsed, setCollapsed] = useState(false) + + const { analyticsEvents } = useAnalytics() const relatedConceptsSource = useMemo(() => ( cdeRelatedConcepts[cde.id] @@ -36,7 +39,10 @@ export const CdeItem = ({ cde, cdeRelatedConcepts, highlight }) => { > { + analyticsEvents.cdeToggled(cde.id, !collapsed) + setCollapsed(collapsed) + } } header={ @@ -75,6 +81,9 @@ export const CdeItem = ({ cde, cdeRelatedConcepts, highlight }) => { { + analyticsEvents.cdeRelatedConceptsToggled(cde.id, expanded) + } } /> {false &&
diff --git a/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concept-options.js b/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concept-options.js index 2b2792da..ee06e8d4 100644 --- a/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concept-options.js +++ b/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concept-options.js @@ -1,9 +1,11 @@ import { Menu, Typography } from 'antd' import { ExportOutlined, SearchOutlined } from '@ant-design/icons' +import { useAnalytics } from '../../../../../../contexts' const { Text } = Typography export const RelatedConceptOptions = ({ concept, openSearch, openConcept, setClosed }) => { + const { analyticsEvents } = useAnalytics() return ( Open ), icon: , - onClick: openConcept, + onClick: () => { + analyticsEvents.cdeRelatedConceptOpened(concept.id) + openConcept() + }, }, { key: 1, @@ -25,7 +30,10 @@ export const RelatedConceptOptions = ({ concept, openSearch, openConcept, setClo Search ), icon: , - onClick: openSearch + onClick: () => { + analyticsEvents.cdeRelatedConceptSearched(concept.id) + openSearch() + } }, ]} /> diff --git a/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concept-tag.css b/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concept-tag.css index 9d014223..92f0a1a1 100644 --- a/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concept-tag.css +++ b/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concept-tag.css @@ -4,6 +4,7 @@ } .related-concept-popover-overlay .ant-popover-inner-content { padding: 0; + width: 100%; } .related-concept-popover-overlay .ant-popover-title { padding: 8px 12px; diff --git a/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concepts-list.js b/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concepts-list.js index f3ea72df..f63418e8 100644 --- a/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concepts-list.js +++ b/src/components/search/concept-modal/tabs/cdes/related-concepts/related-concepts-list.js @@ -7,7 +7,7 @@ const { Text } = Typography const SHOW_COUNT = 8 -export const RelatedConceptsList = ({ concepts, highlight }) => { +export const RelatedConceptsList = ({ concepts, highlight, onShowMore }) => { const [showingAll, setShowingAll] = useState(false) const failed = useMemo(() => concepts === null || concepts.length === 0, [concepts]) @@ -56,8 +56,10 @@ export const RelatedConceptsList = ({ concepts, highlight }) => { size="small" type="link" style={{ fontSize: "12px" }} - onClick={ () => setShowingAll(!showingAll) - } + onClick={ () => { + onShowMore(!showingAll) + setShowingAll(!showingAll) + } } > { !showingAll ? `Show ${hiddenConcepts.length - hiddenHighlighted.length} more results` : "Show less" } diff --git a/src/components/search/concept-modal/tabs/studies/study-variable.js b/src/components/search/concept-modal/tabs/studies/study-variable.js index c9ddeee3..9620de5c 100644 --- a/src/components/search/concept-modal/tabs/studies/study-variable.js +++ b/src/components/search/concept-modal/tabs/studies/study-variable.js @@ -1,16 +1,24 @@ import { Typography } from 'antd' import Highlighter from 'react-highlight-words' +import { useAnalytics } from '../../../../../contexts' const { Text } = Typography -export const StudyVariable = ({ variable, highlight, ...props }) => ( +export const StudyVariable = ({ variable, highlight, ...props }) => { + const { analyticsEvents } = useAnalytics() + + const variableLinkClicked = () => { + analyticsEvents.variableLinkClicked(variable.id) + } + return (
  - ({ variable.e_link ? { variable.id } : variable.id }) + ({ variable.e_link ? { variable.id } : variable.id })
- ) \ No newline at end of file + ) +} \ No newline at end of file diff --git a/src/components/search/concept-modal/tabs/studies/study.js b/src/components/search/concept-modal/tabs/studies/study.js index c3fb3d53..b99ce8a6 100644 --- a/src/components/search/concept-modal/tabs/studies/study.js +++ b/src/components/search/concept-modal/tabs/studies/study.js @@ -4,11 +4,16 @@ import Highlighter from 'react-highlight-words' import { Link } from '../../../../link' import { StudyVariables } from './study-variables' import { StudyVariable } from './study-variable' +import { useAnalytics } from '../../../../../contexts' const { Text, Paragraph } = Typography const { Panel } = Collapse export const Study = ({ study, highlight, collapsed, ...panelProps }) => { + const { analyticsEvents } = useAnalytics() + const studyLinkClicked = () => { + analyticsEvents.studyLinkClicked(study.c_id) + } const highlightedVariables = study.elements.filter((variable) => { return highlight.some((token) => ( variable.name.includes(token) || @@ -21,7 +26,7 @@ export const Study = ({ study, highlight, collapsed, ...panelProps }) => { header={ { ` ` } - ({ study.c_id }) + ({ study.c_id }) } extra={ { study.elements.length } variable{ study.elements.length === 1 ? '' : 's' } } diff --git a/src/components/search/context.js b/src/components/search/context.js index a388101e..8d1292ab 100644 --- a/src/components/search/context.js +++ b/src/components/search/context.js @@ -264,7 +264,7 @@ export const HelxSearch = ({ children }) => { setTotalConcepts(result.total_items) // setConcepts(hits.valid) setIsLoadingConcepts(false) - analyticsEvents.searchExecuted(query, Date.now() - startTime, result.total_items) + // analyticsEvents.searchExecuted(query, Date.now() - startTime, result.total_items) } else { const newConceptPages = { ...conceptPages } newConceptPages[currentPage] = [] @@ -272,7 +272,7 @@ export const HelxSearch = ({ children }) => { setConceptTypes({}) setTotalConcepts(0) setIsLoadingConcepts(false) - analyticsEvents.searchExecuted(query, Date.now() - startTime, 0) + // analyticsEvents.searchExecuted(query, Date.now() - startTime, 0) } } catch (error) { if (error.name !== "CanceledError") { @@ -280,7 +280,7 @@ export const HelxSearch = ({ children }) => { setError({ message: 'An error occurred!' }) setTotalConcepts(0) setIsLoadingConcepts(false) - analyticsEvents.searchExecuted(query, Date.now() - startTime, 0, error) + // analyticsEvents.searchExecuted(query, Date.now() - startTime, 0, error) } } } diff --git a/src/components/search/form/form.js b/src/components/search/form/form.js index 154091b6..7ec79e65 100644 --- a/src/components/search/form/form.js +++ b/src/components/search/form/form.js @@ -6,7 +6,7 @@ import Highlighter from 'react-highlight-words' import Select from 'rc-select' import { useHelxSearch, SearchLayout } from '../' import { SearchCompletion } from './search-suggestion' -import { useEnvironment } from '../../../contexts' +import { useAnalytics, useEnvironment } from '../../../contexts' import { useAbortController } from '../../../hooks' import './form.css' @@ -49,6 +49,7 @@ const highlightedSearchTerms = (highlightedSearch) => { export const SearchForm = ({ type=undefined, ...props }) => { const { context } = useEnvironment() const { doSearch, inputRef, query, totalConcepts, searchHistory, layout, setLayout } = useHelxSearch() + const { analyticsEvents } = useAnalytics() const [searchTerm, setSearchTerm] = useState(query) const [searchSuggestions, setSearchSuggestions] = useState(null) // null = don't appear, [] = "no results found", [...] = show suggestions const [loadingSuggestions, setLoadingSuggestions] = useState(false) // true = loading... @@ -264,6 +265,7 @@ export const SearchForm = ({ type=undefined, ...props }) => { ) : null } onChange={ (e) => { + analyticsEvents.searchTypeChanged(e.target.value) switch (e.target.value) { case "concepts": setLayout(SearchLayout.GRID) diff --git a/src/components/search/results/results-header/index.js b/src/components/search/results/results-header/index.js index a1e41d40..2b8a97f3 100644 --- a/src/components/search/results/results-header/index.js +++ b/src/components/search/results/results-header/index.js @@ -76,7 +76,10 @@ export const ResultsHeader = ({ variables=false, type=FULL, ...props }) => { Filter type: