From f19aa297ac3333e5f6a9b677dcaf79d1b01c5b57 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Fri, 1 Oct 2021 10:02:22 -0700 Subject: [PATCH 1/5] Added ga4 account explorer page. --- src/components/Layout/links.ts | 6 ++++ src/components/ga4/AccountExplorer/index.tsx | 7 +++++ src/hooks/index.ts | 10 +++++++ src/pages/ga4/account-explorer/index.tsx | 31 ++++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 src/components/ga4/AccountExplorer/index.tsx create mode 100644 src/pages/ga4/account-explorer/index.tsx diff --git a/src/components/Layout/links.ts b/src/components/Layout/links.ts index b3bbc0b8f..c0101286b 100644 --- a/src/components/Layout/links.ts +++ b/src/components/Layout/links.ts @@ -36,6 +36,12 @@ export const linkData: LinkData[] = [ type: "link", versions: [GAVersion.UniversalAnalytics], }, + { + text: "Account Explorer", + href: "/ga4/account-explorer/", + type: "link", + versions: [GAVersion.GoogleAnalytics4], + }, { text: "Campaign URL Builder", href: "/campaign-url-builder/", diff --git a/src/components/ga4/AccountExplorer/index.tsx b/src/components/ga4/AccountExplorer/index.tsx new file mode 100644 index 000000000..58f3deede --- /dev/null +++ b/src/components/ga4/AccountExplorer/index.tsx @@ -0,0 +1,7 @@ +import React from "react" + +const AccountExplorer = () => { + return <>hi +} + +export default AccountExplorer diff --git a/src/hooks/index.ts b/src/hooks/index.ts index c4470680c..31a621dd0 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -200,6 +200,11 @@ const getRedirectInfo = ( redirectPath: "/", toast: "Redirecting to the UA home page.", } + case "/ga4/account-explorer/": + return { + redirectPath: "/account-explorer/", + toast: uaToast("Account Explorer"), + } default: return { redirectPath: "/", @@ -249,6 +254,11 @@ const getRedirectInfo = ( redirectPath: "/ga4/", toast: "Redirecting to the GA4 home page.", } + case "/account-explorer/": + return { + redirectPath: "/ga4/account-explorer/", + toast: ga4Toast("Account Explorer"), + } default: return { redirectPath: "/ga4/", diff --git a/src/pages/ga4/account-explorer/index.tsx b/src/pages/ga4/account-explorer/index.tsx new file mode 100644 index 000000000..f3c2dc61e --- /dev/null +++ b/src/pages/ga4/account-explorer/index.tsx @@ -0,0 +1,31 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from "react" + +import Layout from "@/components/Layout" +import AccountExplorer from "@/components/ga4/AccountExplorer" + +export default ({ location: { pathname } }) => { + return ( + + + + ) +} From cf95e7cbdf386131e5ffee9e74ac3a109cbb558f Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Mon, 15 Nov 2021 13:39:34 -0800 Subject: [PATCH 2/5] wip. --- .../ga4/AccountExplorer/ExploreTable.tsx | 60 +++++++++ src/components/ga4/AccountExplorer/index.tsx | 30 ++++- .../ga4/AccountExplorer/useAllAPS.ts | 127 ++++++++++++++++++ src/constants.ts | 4 + 4 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 src/components/ga4/AccountExplorer/ExploreTable.tsx create mode 100644 src/components/ga4/AccountExplorer/useAllAPS.ts diff --git a/src/components/ga4/AccountExplorer/ExploreTable.tsx b/src/components/ga4/AccountExplorer/ExploreTable.tsx new file mode 100644 index 000000000..266ddd30b --- /dev/null +++ b/src/components/ga4/AccountExplorer/ExploreTable.tsx @@ -0,0 +1,60 @@ +import Spinner from "@/components/Spinner" +import React from "react" +import useAllAPS from "./useAllAPS" + +interface ExploreTableProps {} + +const ExploreTable: React.FC = () => { + const aps = useAllAPS() + + if (aps === undefined) { + return Loading accounts + } + return ( + + + {aps.map((aps, aIdx) => + aps.propertySummaries.flatMap((pws, pIdx) => [ + pws.iosStreams === undefined || + pws.webStreams === undefined || + pws.androidStreams === undefined + ? [ + + + + + , + ] + : [ + pws.webStreams.map((s, sIdx) => ( + + + + + + )), + pws.androidStreams.map((s, sIdx) => ( + + + + + + )), + pws.iosStreams.map((s, sIdx) => ( + + + + + + )), + ], + ]) + )} + +
{aps.displayName}{pws.displayName} + Loading streams +
{aps.displayName}{pws.displayName}{s.displayName}
{aps.displayName}{pws.displayName}{s.displayName}
{aps.displayName}{pws.displayName}{s.displayName}
+ ) +} + +export default ExploreTable diff --git a/src/components/ga4/AccountExplorer/index.tsx b/src/components/ga4/AccountExplorer/index.tsx index 58f3deede..526da11b9 100644 --- a/src/components/ga4/AccountExplorer/index.tsx +++ b/src/components/ga4/AccountExplorer/index.tsx @@ -1,7 +1,35 @@ +import { Typography } from "@material-ui/core" import React from "react" +import { StorageKey } from "@/constants" +import useAccountPropertyStream from "../StreamPicker/useAccountPropertyStream" +import StreamPicker from "../StreamPicker" +import ExploreTable from "./ExploreTable" + +enum QueryParam { + Account = "a", + Property = "b", + Stream = "c", +} const AccountExplorer = () => { - return <>hi + const aps = useAccountPropertyStream( + StorageKey.ga4AccountExplorerAPS, + QueryParam + ) + + return ( + <> + Overview + + Use this tool to search or browse through your Google Analytics 4 + accounts, properties, and streams, See what accounts you have access to + and find the IDs that you need for APIs or other tools or services that + integrate with Google Analytics. + + + + + ) } export default AccountExplorer diff --git a/src/components/ga4/AccountExplorer/useAllAPS.ts b/src/components/ga4/AccountExplorer/useAllAPS.ts new file mode 100644 index 000000000..cff35edc2 --- /dev/null +++ b/src/components/ga4/AccountExplorer/useAllAPS.ts @@ -0,0 +1,127 @@ +import { RequestStatus } from "@/types" +import { AccountSummary, PropertySummary } from "@/types/ga4/StreamPicker" +import { useEffect, useMemo, useRef, useState } from "react" +import { useSelector } from "react-redux" +import useAccountSummaries from "../StreamPicker/useAccounts" + +const fetchAllStreams = async ( + adminAPI: typeof gapi.client.analyticsadmin, + property: string +) => { + const [ + { + result: { webDataStreams: webStreams = [] }, + }, + { + result: { iosAppDataStreams: iosStreams = [] }, + }, + { + result: { androidAppDataStreams: androidStreams = [] }, + }, + ] = await Promise.all([ + adminAPI.properties.webDataStreams.list({ + parent: property, + }), + adminAPI.properties.iosAppDataStreams.list({ + parent: property, + }), + adminAPI.properties.androidAppDataStreams.list({ + parent: property, + }), + ]) + // TODO - consider handling pagination, though it seems very unlikely there + // will be _pages_ of streams for a given property. + return { webStreams, iosStreams, androidStreams } +} + +interface PropertyWithStreams extends PropertySummary { + webStreams?: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaWebDataStream[] + androidStreams?: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaAndroidAppDataStream[] + iosStreams?: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaIosAppDataStream[] +} + +export interface AccountWithStreams extends AccountSummary { + propertySummaries: Array +} + +const useAllAPS = () => { + const gapi = useSelector((a: AppState) => a.gapi) + const adminAPI = useMemo(() => gapi?.client?.analyticsadmin, [gapi]) + const accountSummariesRequest = useAccountSummaries() + const accounts = useMemo( + () => + accountSummariesRequest.status === RequestStatus.Successful + ? accountSummariesRequest.accounts + : undefined, + [accountSummariesRequest] + ) + const [allAPS, setAllAPS] = useState( + accounts as AccountWithStreams[] + ) + + console.log(allAPS) + + const shouldRun = useRef(true) + + useEffect(() => { + shouldRun.current = true + if (adminAPI === undefined || accounts === undefined) { + return + } + if (accounts.length === 0) { + return + } + ;(async () => { + for (let accountIdx = 0; accountIdx < accounts.length; accountIdx++) { + if (shouldRun.current) { + const currentAccount = accounts[accountIdx] + const currentProperties = currentAccount.propertySummaries + if ( + currentProperties === undefined || + currentProperties.length === 0 + ) { + return + } + for ( + let propertyIdx = 0; + propertyIdx < currentProperties.length; + propertyIdx++ + ) { + if (shouldRun.current) { + const currentProperty = currentProperties[propertyIdx] + const { + webStreams, + androidStreams, + iosStreams, + } = await fetchAllStreams(adminAPI, currentProperty.property!) + setAllAPS((old = []) => { + return old.map((aws, aIdx) => + aIdx === accountIdx + ? { + ...aws, + propertySummaries: ( + aws.propertySummaries || [] + ).map((pws, pIdx) => + pIdx === propertyIdx + ? { ...pws, iosStreams, androidStreams, webStreams } + : pws + ), + } + : aws + ) + }) + } + } + } + } + })() + + return () => { + shouldRun.current = false + } + }, [accounts, adminAPI, setAllAPS]) + + return allAPS +} + +export default useAllAPS diff --git a/src/constants.ts b/src/constants.ts index 03c1e02af..5f7b81fe5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -248,6 +248,10 @@ export enum StorageKey { ga4EventBuilderItems = "ga4/event-builder/items", ga4EventBuilderEventName = "ga4/event-builder/event-name", ga4EventBuilderUserProperties = "ga4/event-builder/user-properties", + + // GA4 Account Explorer + ga4AccountExplorerAPS = "ga4/account-explorer/aps", + allAPS = "ga4/account-explorer/all-aps", } export const EventAction = { From 313044bf27aeda5deeb85e47cde00b016f31526d Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Tue, 7 Dec 2021 14:10:32 -0800 Subject: [PATCH 3/5] did a bit more styling. --- .../ga4/AccountExplorer/ExploreTable.tsx | 253 +++++++++++++++--- .../ga4/AccountExplorer/useAllAPS.ts | 2 - 2 files changed, 210 insertions(+), 45 deletions(-) diff --git a/src/components/ga4/AccountExplorer/ExploreTable.tsx b/src/components/ga4/AccountExplorer/ExploreTable.tsx index 266ddd30b..08474b046 100644 --- a/src/components/ga4/AccountExplorer/ExploreTable.tsx +++ b/src/components/ga4/AccountExplorer/ExploreTable.tsx @@ -1,59 +1,226 @@ +import { CopyIconButton } from "@/components/CopyButton" import Spinner from "@/components/Spinner" +import { + Box, + makeStyles, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from "@material-ui/core" +import { Android, Apple, Language } from "@material-ui/icons" import React from "react" import useAllAPS from "./useAllAPS" interface ExploreTableProps {} +const WebCell: React.FC<{ + stream: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaWebDataStream +}> = ({ stream }) => { + return ( + <> + + + {stream.displayName} + + {stream.name?.substring(stream.name?.lastIndexOf("/") + 1)} + + + + ) +} + +const AndroidCell: React.FC<{ + stream: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaAndroidAppDataStream +}> = ({ stream }) => { + return ( + <> + + + + {stream.displayName || stream.packageName} + + + {stream.name?.substring(stream.name?.lastIndexOf("/") + 1)} + + + + ) +} + +const IOSCell: React.FC<{ + stream: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaIosAppDataStream +}> = ({ stream }) => { + return ( + <> + + + + {stream.displayName || stream.bundleId} + + + {stream.name?.substring(stream.name?.lastIndexOf("/") + 1)} + + + + ) +} + +const useStyles = makeStyles(theme => ({ + streamCell: { + display: "flex", + alignItems: "center", + "&> svg": { + marginRight: theme.spacing(1), + }, + "&> button": { + marginLeft: "auto", + }, + "&> div > p": { + margin: "unset", + padding: "unset", + }, + }, +})) + const ExploreTable: React.FC = () => { const aps = useAllAPS() + const classes = useStyles() if (aps === undefined) { return Loading accounts } + return ( - - - {aps.map((aps, aIdx) => - aps.propertySummaries.flatMap((pws, pIdx) => [ - pws.iosStreams === undefined || - pws.webStreams === undefined || - pws.androidStreams === undefined - ? [ - - - - - , - ] - : [ - pws.webStreams.map((s, sIdx) => ( - - - - - - )), - pws.androidStreams.map((s, sIdx) => ( - - - - - - )), - pws.iosStreams.map((s, sIdx) => ( - - - - - - )), - ], - ]) - )} - -
{aps.displayName}{pws.displayName} - Loading streams -
{aps.displayName}{pws.displayName}{s.displayName}
{aps.displayName}{pws.displayName}{s.displayName}
{aps.displayName}{pws.displayName}{s.displayName}
+ + + + Account + Property + Stream + + + + {aps.map(a => ( + + {a.propertySummaries.flatMap(p => { + const Wrapper: React.FC = ({ children }) => ( + + + + + {a.displayName} + + {a.name?.substring(a.name?.lastIndexOf("/") + 1)} + + + + + + + + + {p.displayName} + + {p.property?.substring( + p.property?.lastIndexOf("/") + 1 + )} + + + + + + + {children} + + + ) + + const rows: JSX.Element[] = [] + const baseKey = `${a.account}-${p.property}` + + if (p.webStreams === undefined) { + rows.push( + Loading... + ) + } else { + p.webStreams.forEach(s => + rows.push( + + + + + ) + ) + } + + if (p.androidStreams === undefined) { + rows.push( + + Loading... + + ) + } else { + p.androidStreams.forEach(s => + rows.push( + + + + + ) + ) + } + + if (p.iosStreams === undefined) { + rows.push( + Loading... + ) + } else { + p.iosStreams.forEach(s => + rows.push( + + + + + ) + ) + } + + return rows + })} + + ))} + +
) } diff --git a/src/components/ga4/AccountExplorer/useAllAPS.ts b/src/components/ga4/AccountExplorer/useAllAPS.ts index cff35edc2..44c57ca38 100644 --- a/src/components/ga4/AccountExplorer/useAllAPS.ts +++ b/src/components/ga4/AccountExplorer/useAllAPS.ts @@ -59,8 +59,6 @@ const useAllAPS = () => { accounts as AccountWithStreams[] ) - console.log(allAPS) - const shouldRun = useRef(true) useEffect(() => { From 8411a93a3f38b1e875c597e30ac8c4374637e4a1 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Thu, 30 Dec 2021 11:48:24 -0800 Subject: [PATCH 4/5] implemented filtering when accounts/properties are picked. --- .../ga4/AccountExplorer/ExploreTable.tsx | 15 +++++++++++++-- src/components/ga4/AccountExplorer/index.tsx | 10 ++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/ga4/AccountExplorer/ExploreTable.tsx b/src/components/ga4/AccountExplorer/ExploreTable.tsx index 08474b046..5ab890197 100644 --- a/src/components/ga4/AccountExplorer/ExploreTable.tsx +++ b/src/components/ga4/AccountExplorer/ExploreTable.tsx @@ -12,9 +12,10 @@ import { } from "@material-ui/core" import { Android, Apple, Language } from "@material-ui/icons" import React from "react" +import { AccountProperty } from "../StreamPicker/useAccountProperty" import useAllAPS from "./useAllAPS" -interface ExploreTableProps {} +interface ExploreTableProps extends AccountProperty {} const WebCell: React.FC<{ stream: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaWebDataStream @@ -85,7 +86,7 @@ const useStyles = makeStyles(theme => ({ }, })) -const ExploreTable: React.FC = () => { +const ExploreTable: React.FC = ({ account, property }) => { const aps = useAllAPS() const classes = useStyles() @@ -153,6 +154,16 @@ const ExploreTable: React.FC = () => { const rows: JSX.Element[] = [] const baseKey = `${a.account}-${p.property}` + if (account !== undefined) { + if (a.account !== account.account) { + return null + } + if (property !== undefined) { + if (p.property !== property.property) { + return null + } + } + } if (p.webStreams === undefined) { rows.push( Loading... diff --git a/src/components/ga4/AccountExplorer/index.tsx b/src/components/ga4/AccountExplorer/index.tsx index 526da11b9..68e67563e 100644 --- a/src/components/ga4/AccountExplorer/index.tsx +++ b/src/components/ga4/AccountExplorer/index.tsx @@ -4,6 +4,7 @@ import { StorageKey } from "@/constants" import useAccountPropertyStream from "../StreamPicker/useAccountPropertyStream" import StreamPicker from "../StreamPicker" import ExploreTable from "./ExploreTable" +import useAccountProperty from "../StreamPicker/useAccountProperty" enum QueryParam { Account = "a", @@ -12,10 +13,7 @@ enum QueryParam { } const AccountExplorer = () => { - const aps = useAccountPropertyStream( - StorageKey.ga4AccountExplorerAPS, - QueryParam - ) + const ap = useAccountProperty(StorageKey.ga4AccountExplorerAPS, QueryParam) return ( <> @@ -26,8 +24,8 @@ const AccountExplorer = () => { and find the IDs that you need for APIs or other tools or services that integrate with Google Analytics. - - + + ) } From 9aaf08047fefa4edc9d11658d3d5867aa73c79f8 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Thu, 30 Dec 2021 12:21:55 -0800 Subject: [PATCH 5/5] got this to a workable state. Still needs some perf work, though. --- src/components/ga4/AccountExplorer/index.tsx | 12 ++-- .../ga4/AccountExplorer/useAllAPS.ts | 61 ++++++++++++++++--- src/constants.ts | 1 + 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/components/ga4/AccountExplorer/index.tsx b/src/components/ga4/AccountExplorer/index.tsx index 68e67563e..2da1f8dc0 100644 --- a/src/components/ga4/AccountExplorer/index.tsx +++ b/src/components/ga4/AccountExplorer/index.tsx @@ -1,7 +1,7 @@ -import { Typography } from "@material-ui/core" import React from "react" + +import { Typography } from "@material-ui/core" import { StorageKey } from "@/constants" -import useAccountPropertyStream from "../StreamPicker/useAccountPropertyStream" import StreamPicker from "../StreamPicker" import ExploreTable from "./ExploreTable" import useAccountProperty from "../StreamPicker/useAccountProperty" @@ -19,10 +19,10 @@ const AccountExplorer = () => { <> Overview - Use this tool to search or browse through your Google Analytics 4 - accounts, properties, and streams, See what accounts you have access to - and find the IDs that you need for APIs or other tools or services that - integrate with Google Analytics. + Use this tool to browse through your Google Analytics 4 accounts, + properties, and streams, See what accounts you have access to and find + the IDs that you need for APIs or other tools or services that integrate + with Google Analytics 4. diff --git a/src/components/ga4/AccountExplorer/useAllAPS.ts b/src/components/ga4/AccountExplorer/useAllAPS.ts index 44c57ca38..9831810b0 100644 --- a/src/components/ga4/AccountExplorer/useAllAPS.ts +++ b/src/components/ga4/AccountExplorer/useAllAPS.ts @@ -1,12 +1,24 @@ +import { StorageKey } from "@/constants" import { RequestStatus } from "@/types" import { AccountSummary, PropertySummary } from "@/types/ga4/StreamPicker" +import moment from "moment" import { useEffect, useMemo, useRef, useState } from "react" import { useSelector } from "react-redux" import useAccountSummaries from "../StreamPicker/useAccounts" +const maxAge = moment.duration(30, "minutes") +type StreamsForProperty = { + webStreams: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaWebDataStream[] + iosStreams: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaIosAppDataStream[] + androidStreams: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaAndroidAppDataStream[] + property: string + timestamp: number +} + const fetchAllStreams = async ( adminAPI: typeof gapi.client.analyticsadmin, - property: string + property: string, + key: string ) => { const [ { @@ -31,7 +43,18 @@ const fetchAllStreams = async ( ]) // TODO - consider handling pagination, though it seems very unlikely there // will be _pages_ of streams for a given property. - return { webStreams, iosStreams, androidStreams } + const fetchTime = moment.now() + const nu: StreamsForProperty = { + webStreams, + iosStreams, + androidStreams, + property, + timestamp: fetchTime, + } + + window.localStorage.setItem(key, JSON.stringify(nu)) + + return nu } interface PropertyWithStreams extends PropertySummary { @@ -87,11 +110,35 @@ const useAllAPS = () => { ) { if (shouldRun.current) { const currentProperty = currentProperties[propertyIdx] - const { - webStreams, - androidStreams, - iosStreams, - } = await fetchAllStreams(adminAPI, currentProperty.property!) + + const key = StorageKey.ga4Streams + currentProperty.property! + const fromCache = window.localStorage.getItem(key) + let stuff: StreamsForProperty + if (fromCache !== null) { + const parsed: StreamsForProperty = JSON.parse(fromCache) + const now = moment() + const cacheTime = moment(parsed.timestamp) + if ( + cacheTime === undefined || + now.isAfter(moment(cacheTime).add(maxAge)) + ) { + console.log("should update") + stuff = await fetchAllStreams( + adminAPI, + currentProperty.property!, + key + ) + } else { + stuff = parsed + } + } else { + stuff = await fetchAllStreams( + adminAPI, + currentProperty.property!, + key + ) + } + const { webStreams, androidStreams, iosStreams } = stuff setAllAPS((old = []) => { return old.map((aws, aIdx) => aIdx === accountIdx diff --git a/src/constants.ts b/src/constants.ts index 5f7b81fe5..102db4171 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -252,6 +252,7 @@ export enum StorageKey { // GA4 Account Explorer ga4AccountExplorerAPS = "ga4/account-explorer/aps", allAPS = "ga4/account-explorer/all-aps", + ga4Streams = "ga4/account-explorer/streams-for-property/", } export const EventAction = {