diff --git a/packages/protocol-dashboard/src/components/ApiCallsStat/ApiCallsStat.tsx b/packages/protocol-dashboard/src/components/ApiCallsStat/ApiCallsStat.tsx index 7427e24bbf3..270145276c8 100644 --- a/packages/protocol-dashboard/src/components/ApiCallsStat/ApiCallsStat.tsx +++ b/packages/protocol-dashboard/src/components/ApiCallsStat/ApiCallsStat.tsx @@ -13,16 +13,14 @@ type OwnProps = {} type ApiCallsStatProps = OwnProps -const ApiCallsStat: React.FC = ( - props: ApiCallsStatProps -) => { +const ApiCallsStat: React.FC = () => { const { apiCalls } = useTrailingApiCalls(Bucket.MONTH) let error, stat if (apiCalls === MetricError.ERROR) { error = true stat = null } else { - stat = apiCalls?.count ?? null + stat = apiCalls?.total_count ?? null } return ( { pointRadius: 0, pointHitRadius: 8 } - let solidLine = data.slice(0, -1) as (number | undefined)[] - if (showLeadingDay) { - solidLine = solidLine.concat([undefined]) - } + const newLabels = showLeadingDay ? labels : labels.slice(0, -1) + const solidLine = showLeadingDay ? data : data.slice(0, -1) const datasets = [ { ...common, @@ -58,21 +56,6 @@ const getData = (data: number[], labels: string[], showLeadingDay: boolean) => { } ] - if (showLeadingDay) { - const dottedLine = new Array(Math.max(data.length - 2, 0)) - .fill(undefined) - .concat(data.slice(-2)) - - datasets.push({ - ...common, - label: 'current', - data: dottedLine, - borderDash: [2, 6] - }) - } - - const newLabels = showLeadingDay ? labels : labels.slice(0, -1) - return { labels: newLabels, datasets diff --git a/packages/protocol-dashboard/src/components/ServiceTable/ServiceTable.tsx b/packages/protocol-dashboard/src/components/ServiceTable/ServiceTable.tsx index ee24137bbf0..b74a864c044 100644 --- a/packages/protocol-dashboard/src/components/ServiceTable/ServiceTable.tsx +++ b/packages/protocol-dashboard/src/components/ServiceTable/ServiceTable.tsx @@ -5,22 +5,16 @@ import styles from './ServiceTable.module.css' import Table from 'components/Table' import Error from 'components/Error' -export type ServiceRow = { +type ServiceRow = { endpoint: string version: string } -type Service = { - endpoint: string - version: string - spID: number -} - type OwnProps = { className?: string isLoading?: boolean title: string - data: Array + data: ServiceRow[] limit?: number moreText?: string onRowClick: diff --git a/packages/protocol-dashboard/src/components/TopAPIAppsChart/TopAPIAppsChart.tsx b/packages/protocol-dashboard/src/components/TopAPIAppsChart/TopAPIAppsChart.tsx index 17f8ea0b1a3..2ec6892c87a 100644 --- a/packages/protocol-dashboard/src/components/TopAPIAppsChart/TopAPIAppsChart.tsx +++ b/packages/protocol-dashboard/src/components/TopAPIAppsChart/TopAPIAppsChart.tsx @@ -12,7 +12,7 @@ type OwnProps = { } const messages = { - title: 'Top API Apps by Total Requests', + title: 'Top 3rd Party API Apps by Total Requests', yLabel: 'Total Requests', viewMore: 'View Full Leaderboard' } diff --git a/packages/protocol-dashboard/src/components/TopAPITable/TopAPITable.tsx b/packages/protocol-dashboard/src/components/TopAPITable/TopAPITable.tsx index 56940d1fc17..e535222fdfe 100644 --- a/packages/protocol-dashboard/src/components/TopAPITable/TopAPITable.tsx +++ b/packages/protocol-dashboard/src/components/TopAPITable/TopAPITable.tsx @@ -9,16 +9,11 @@ import { Bucket, MetricError } from 'store/cache/analytics/slice' import { useIsMobile } from 'utils/hooks' const messages = { - title: 'Top API Apps by Total Requests', + title: 'Top 3rd Party API Apps by Total Requests', rank: 'Rank', totalReq: 'Total Requests' } -export type ServiceRow = { - endpoint: string - version: string -} - export type APIAppRequests = { rank: number name: string @@ -41,7 +36,6 @@ const filterCount = (name: string, count: number): boolean => const TopAPITable: React.FC = ({ className, - limit, alwaysShowMore }: TopAPITableProps) => { const isMobile = useIsMobile() diff --git a/packages/protocol-dashboard/src/components/TopAppsChart/TopAppsChart.tsx b/packages/protocol-dashboard/src/components/TopAppsChart/TopAppsChart.tsx index 63dbb6209cf..cb8bce01a3e 100644 --- a/packages/protocol-dashboard/src/components/TopAppsChart/TopAppsChart.tsx +++ b/packages/protocol-dashboard/src/components/TopAppsChart/TopAppsChart.tsx @@ -30,7 +30,7 @@ const TopAppsChart: React.FC = () => { return ( = props => { labels = [] data = [] } else { - labels = apiCalls?.map(a => a.timestamp) ?? null - data = apiCalls?.map(a => a.count) ?? null + labels = apiCalls?.map(a => new Date(a.timestamp).getTime() / 1000) ?? null + data = apiCalls?.map(a => a.total_count) ?? null } return ( = props => { selection={bucket} options={[Bucket.ALL_TIME, Bucket.MONTH, Bucket.WEEK]} onSelectOption={(option: string) => setBucket(option as Bucket)} + showLeadingDay /> ) } diff --git a/packages/protocol-dashboard/src/components/UniqueUsersChart/UniqueUsersChart.tsx b/packages/protocol-dashboard/src/components/UniqueUsersChart/UniqueUsersChart.tsx index 117224c8ba4..f119d8ddb0e 100644 --- a/packages/protocol-dashboard/src/components/UniqueUsersChart/UniqueUsersChart.tsx +++ b/packages/protocol-dashboard/src/components/UniqueUsersChart/UniqueUsersChart.tsx @@ -5,9 +5,9 @@ import { Bucket, MetricError } from 'store/cache/analytics/slice' type OwnProps = {} -type TotalApiCallsChartProps = OwnProps +type UniqueUsersChartProps = OwnProps -const TotalApiCallsChart: React.FC = () => { +const UniqueUsersChart: React.FC = () => { const [bucket, setBucket] = useState(Bucket.MONTH) const { apiCalls } = useApiCalls(bucket) @@ -17,8 +17,8 @@ const TotalApiCallsChart: React.FC = () => { labels = [] data = [] } else { - labels = apiCalls?.map(a => a.timestamp) ?? null - data = apiCalls?.map(a => a.unique_count || 0) ?? null + labels = apiCalls?.map(a => new Date(a.timestamp).getTime() / 1000) ?? null + data = apiCalls?.map(a => a.summed_unique_count) ?? null } return ( = () => { error={error} options={[Bucket.ALL_TIME, Bucket.MONTH, Bucket.WEEK]} onSelectOption={(option: string) => setBucket(option as Bucket)} + showLeadingDay /> ) } -export default TotalApiCallsChart +export default UniqueUsersChart diff --git a/packages/protocol-dashboard/src/components/UniqueUsersStat/UniqueUsersStat.tsx b/packages/protocol-dashboard/src/components/UniqueUsersStat/UniqueUsersStat.tsx index fcfa015e384..aa2c930808d 100644 --- a/packages/protocol-dashboard/src/components/UniqueUsersStat/UniqueUsersStat.tsx +++ b/packages/protocol-dashboard/src/components/UniqueUsersStat/UniqueUsersStat.tsx @@ -13,16 +13,14 @@ type OwnProps = {} type UniqueUsersStatProps = OwnProps -const UniqueUsersStat: React.FC = ( - props: UniqueUsersStatProps -) => { +const UniqueUsersStat: React.FC = () => { const { apiCalls } = useTrailingApiCalls(Bucket.MONTH) let error, stat if (apiCalls === MetricError.ERROR) { error = true stat = null } else { - stat = apiCalls?.unique_count ?? null + stat = apiCalls?.summed_unique_count ?? null } return ( const getStartTime = (bucket: Bucket, clampDays: boolean = false) => { switch (bucket) { case Bucket.ALL_TIME: - return dayjs() - .subtract(1, 'year') - .startOf('hour') - .unix() case Bucket.YEAR: return dayjs() .subtract(1, 'year') @@ -96,52 +91,6 @@ const getStartTime = (bucket: Bucket, clampDays: boolean = false) => { } } -const joinCountDatasets = (datasets: CountRecord[]) => { - const result: CountRecord = {} - - for (let dataset of datasets) { - Object.keys(dataset).forEach(key => { - if (key in result) { - result[key] += dataset[key] - } else { - result[key] = dataset[key] - } - }) - } - - return result -} - -const joinTimeSeriesDatasets = (datasets: TimeSeriesRecord[][]) => { - if (!datasets.length) return [] - const joined: TimeSeriesRecord[] = [] - - // Joined dataset should be of length "max" of each child dataset - let maxLength = 0 - let maxIndex = 0 - datasets.forEach((dataset, i) => { - if (dataset.length > maxLength) { - maxLength = dataset.length - maxIndex = i - } - }) - for (let i = 0; i < maxLength; ++i) { - const { timestamp } = datasets[maxIndex][i] - let count: number = 0 - let unique_count: number = 0 - datasets.forEach(dataset => { - if (!dataset[i]) return - count += dataset[i].count - if (dataset[i].unique_count) { - unique_count += dataset[i].unique_count! - } - }) - joined.push({ timestamp, count, unique_count }) - } - - return joined -} - // -------------------------------- Selectors --------------------------------- export const getApiCalls = (state: AppState, { bucket }: { bucket: Bucket }) => state.cache.analytics.apiCalls ? state.cache.analytics.apiCalls[bucket] : null @@ -173,36 +122,29 @@ export const getTopApps = (state: AppState, { bucket }: { bucket: Bucket }) => // -------------------------------- Thunk Actions --------------------------------- -async function fetchTimeSeries( - route: string, +async function fetchRoutesTimeSeries( bucket: Bucket, - nodes: DiscoveryProvider[], - clampDays: boolean = true + nodes: DiscoveryProvider[] ) { - const startTime = getStartTime(bucket, clampDays) let error = false - const datasets = ( - await Promise.all( - nodes.map(async node => { - try { - const bucket_size = BUCKET_GRANULARITY_MAP[bucket] - const url = `${node.endpoint}/v1/metrics/${route}?bucket_size=${bucket_size}&start_time=${startTime}` - const res = await fetchWithTimeout(url) - return res.data - } catch (e) { - console.error(e) - error = true - return null - } - }) + let metric: TimeSeriesRecord[] = [] + try { + const bucket_size = BUCKET_GRANULARITY_MAP[bucket] + const res = await fetchUntilSuccess( + nodes.map( + node => + `${node.endpoint}/v1/metrics/aggregates/routes/${bucket}?bucket_size=${bucket_size}` + ) ) - ).filter(Boolean) - + metric = res.data + } catch (e) { + console.error(e) + error = true + } if (error) { return MetricError.ERROR } - const metric = joinTimeSeriesDatasets(datasets).reverse() return metric } @@ -210,23 +152,47 @@ export function fetchApiCalls( bucket: Bucket, nodes: DiscoveryProvider[] ): ThunkAction> { - return async (dispatch, getState, aud) => { - const metric = await fetchTimeSeries('routes', bucket, nodes) + return async dispatch => { + const metric = await fetchRoutesTimeSeries(bucket, nodes) dispatch(setApiCalls({ metric, bucket })) } } +async function fetchTimeSeries( + route: string, + bucket: Bucket, + nodes: DiscoveryProvider[], + clampDays: boolean = true +) { + const startTime = getStartTime(bucket, clampDays) + let error = false + let metric: TimeSeriesRecord[] = [] + try { + const bucket_size = BUCKET_GRANULARITY_MAP[bucket] + const res = await fetchUntilSuccess( + nodes.map( + node => + `${node.endpoint}/v1/metrics/${route}?bucket_size=${bucket_size}&start_time=${startTime}` + ) + ) + metric = res.data.reverse() + } catch (e) { + console.error(e) + error = true + } + if (error) { + return MetricError.ERROR + } + + return metric +} + export function fetchPlays( bucket: Bucket, nodes: DiscoveryProvider[] ): ThunkAction> { - return async (dispatch, getState, aud) => { - const metric = await fetchTimeSeries( - 'plays', - bucket, - nodes.slice(0, 1), - true - ) + return async dispatch => { + const metric = await fetchTimeSeries('plays', bucket, nodes, true) dispatch(setPlays({ metric, bucket })) } } @@ -289,24 +255,16 @@ export function fetchTotalStaked( } } -const getTrailingAPI = (endpoint: string) => async () => { - const url = `${endpoint}/v1/metrics/routes/trailing/month` - const json = await fetchWithTimeout(url) - return { - count: json?.data?.count ?? 0, - unique_count: json?.data?.unique_count ?? 0 - } as CountRecord -} - -const getTrailingAPILegacy = ( - endpoint: string, - startTime: number -) => async () => { - const url = `${endpoint}/v1/metrics/routes?bucket_size=century&start_time=${startTime}` - const json = await fetchWithTimeout(url) +const getTrailingAPI = async (nodes: DiscoveryProvider[]) => { + const json = await fetchUntilSuccess( + nodes.map( + node => `${node.endpoint}/v1/metrics/aggregates/routes/trailing/month` + ) + ) return { - count: json?.data?.[0]?.count ?? 0, - unique_count: json?.data?.[0]?.unique_count ?? 0 + total_count: json?.data?.total_count ?? 0, + unique_count: json?.data?.unique_count ?? 0, + summed_unique_count: json?.data?.summed_unique_count ?? 0 } as CountRecord } @@ -314,63 +272,35 @@ export function fetchTrailingApiCalls( bucket: Bucket, nodes: DiscoveryProvider[] ): ThunkAction> { - return async (dispatch, getState, aud) => { - const startTime = getStartTime(bucket) + return async dispatch => { let error = false - const datasets = ( - await Promise.all( - nodes.map(async node => { - try { - const res = await performWithFallback( - getTrailingAPI(node.endpoint), - getTrailingAPILegacy(node.endpoint, startTime) - ) - return res - } catch (e) { - console.error(e) - error = true - return {} - } - }) - ) - ).filter(Boolean) + let metric = {} + try { + metric = await getTrailingAPI(nodes) + } catch (e) { + console.error(e) + error = true + } if (error) { dispatch(setTrailingApiCalls({ metric: MetricError.ERROR, bucket })) return } - const metric = joinCountDatasets(datasets) - dispatch(setTrailingApiCalls({ metric, bucket })) } } -const getTrailingTopApps = ( - endpoint: string, +const getTrailingTopApps = async ( + nodes: DiscoveryProvider[], bucket: Bucket, limit: number -) => async () => { - const bucketPaths: { [bucket: string]: string | undefined } = { - [Bucket.WEEK]: 'week', - [Bucket.MONTH]: 'month', - [Bucket.ALL_TIME]: 'all_time' - } - const bucketPath = bucketPaths[bucket] - if (!bucketPath) throw new Error('Invalid bucket') - const url = `${endpoint}/v1/metrics/app_name/trailing/${bucketPath}?limit=${limit}` - const json = await fetchWithTimeout(url) - if (!json.data) return {} - return json -} - -const getTopAppsLegacy = ( - endpoint: string, - startTime: number, - limit: number -) => async () => { - const url = `${endpoint}/v1/metrics/app_name?start_time=${startTime}&limit=${limit}&include_unknown=true` - const json = await fetchWithTimeout(url) - if (!json.data) return {} - return json +) => { + const json = await fetchUntilSuccess( + nodes.map( + node => + `${node.endpoint}/v1/metrics/aggregates/apps/${bucket}?limit=${limit}` + ) + ) + return json.data as { name: string; count: number }[] } export function fetchTopApps( @@ -378,43 +308,32 @@ export function fetchTopApps( nodes: DiscoveryProvider[], limit: number = 500 ): ThunkAction> { - return async (dispatch, getState, aud) => { - const startTime = getStartTime(bucket) + return async dispatch => { let error = false - const datasets = ( - await Promise.all( - nodes.map(async node => { - try { - const res = await performWithFallback( - getTrailingTopApps(node.endpoint, bucket, limit), - getTopAppsLegacy(node.endpoint, startTime, limit) - ) - if (!res.data) return {} - let apps: CountRecord = {} - res.data.forEach((app: { name: string; count: number }) => { - const name = - app.name === 'unknown' ? 'Audius Apps + Unknown' : app.name - if (app.count > 0) { - apps[name] = app.count - } - }) - return apps - } catch (e) { - console.error(e) - error = true - return {} + let metric: CountRecord = {} + try { + const res = await getTrailingTopApps(nodes, bucket, limit) + if (res) { + let apps: CountRecord = {} + res.forEach((app: { name: string; count: number }) => { + const name = + app.name === 'unknown' ? 'Audius Apps + Unknown' : app.name + if (app.count > 0) { + apps[name] = app.count } }) - ) - ).filter(Boolean) + metric = apps + } + } catch (e) { + console.error(e) + error = true + } if (error) { dispatch(setTopApps({ metric: MetricError.ERROR, bucket })) return } - const metric = joinCountDatasets(datasets) - const keys = Object.keys(metric) keys.sort((a, b) => { return metric[b] - metric[a] @@ -432,7 +351,7 @@ export function fetchTrailingTopGenres( bucket: Bucket, nodes: DiscoveryProvider[] ): ThunkAction> { - return async (dispatch, getState, aud) => { + return async dispatch => { const node = nodes[0] if (!node) return try { @@ -647,29 +566,26 @@ export const useTopApps = ( limit?: number, filter?: (name: string, count: number) => boolean ) => { - const [doOnce, setDoOnce] = useState(null) + const [hasFetched, setHasFetched] = useState(false) let topApps = useSelector(state => getTopApps(state as AppState, { bucket })) const { nodes } = useDiscoveryProviders({}) const dispatch = useDispatch() useEffect(() => { if ( - doOnce !== bucket && + !hasFetched && nodes.length && (topApps === null || topApps === undefined || limit === undefined || Object.keys(topApps).length < limit) ) { - setDoOnce(bucket) dispatch(fetchTopApps(bucket, nodes, limit)) } - }, [dispatch, topApps, bucket, nodes, doOnce, limit]) + }, [dispatch, topApps, bucket, nodes, hasFetched, limit]) useEffect(() => { - if (topApps) { - setDoOnce(null) - } - }, [topApps, setDoOnce]) + setHasFetched(!!topApps) + }, [topApps, setHasFetched]) if (filter && topApps && topApps !== MetricError.ERROR) { topApps = filterTopApps(topApps, filter) diff --git a/packages/protocol-dashboard/src/store/cache/analytics/slice.ts b/packages/protocol-dashboard/src/store/cache/analytics/slice.ts index f137169e6f3..3c2bdfa27fb 100644 --- a/packages/protocol-dashboard/src/store/cache/analytics/slice.ts +++ b/packages/protocol-dashboard/src/store/cache/analytics/slice.ts @@ -2,8 +2,10 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' export type TimeSeriesRecord = { timestamp: string - count: number + count?: number + total_count?: number unique_count?: number + summed_unique_count?: number } export type CountRecord = { @@ -11,8 +13,8 @@ export type CountRecord = { } export enum Bucket { - ALL_TIME = 'all time', // Granularity: year - YEAR = 'year', // Ganularity: month + ALL_TIME = 'all_time', // Granularity: year + YEAR = 'year', // Granularity: month MONTH = 'month', // Granularity: week WEEK = 'week', // Granularity: day DAY = 'day' // Granularity: hour diff --git a/packages/protocol-dashboard/src/store/cache/discoveryProvider/hooks.ts b/packages/protocol-dashboard/src/store/cache/discoveryProvider/hooks.ts index 70e190f14f8..e1164403047 100644 --- a/packages/protocol-dashboard/src/store/cache/discoveryProvider/hooks.ts +++ b/packages/protocol-dashboard/src/store/cache/discoveryProvider/hooks.ts @@ -92,10 +92,16 @@ const processDP = async ( // -------------------------------- Thunk Actions -------------------------------- // Async function to get -export function fetchDiscoveryProviders( - props: UseDiscoveryProvidersProps = {} -): ThunkAction> { +export function fetchDiscoveryProviders(): ThunkAction< + void, + AppState, + Audius, + Action +> { return async (dispatch, getState, aud) => { + const status = getStatus(getState()) + if (status) return + dispatch(setLoading()) const discoveryProviders = await aud.ServiceProviderClient.getServiceProviderList( ServiceType.DiscoveryProvider @@ -172,9 +178,9 @@ export const useDiscoveryProviders = ({ const dispatch = useDispatch() useEffect(() => { if (!status) { - dispatch(fetchDiscoveryProviders({ owner, sortBy, limit })) + dispatch(fetchDiscoveryProviders()) } - }, [dispatch, status, owner, sortBy, limit]) + }, [dispatch, status]) return { status, nodes } } diff --git a/packages/protocol-dashboard/src/utils/fetch.ts b/packages/protocol-dashboard/src/utils/fetch.ts index 432b12cea50..17de8e62a15 100644 --- a/packages/protocol-dashboard/src/utils/fetch.ts +++ b/packages/protocol-dashboard/src/utils/fetch.ts @@ -27,3 +27,15 @@ export const withTimeout = async ( const res = await Promise.race([asyncCall, timeoutPromise]) return res } + +export const fetchUntilSuccess = async (endpoints: string[]): Promise => { + // Pick a random endpoint and make a call to that endpoint + const endpoint = endpoints[Math.floor(Math.random() * endpoints.length)] + console.info('Attempting endpoint: ', endpoint) + try { + return await fetchWithTimeout(endpoint) + } catch (e) { + console.error(e) + return await fetchUntilSuccess(endpoints) + } +} diff --git a/packages/protocol-dashboard/src/utils/performWithFallback.ts b/packages/protocol-dashboard/src/utils/performWithFallback.ts deleted file mode 100644 index 4c6b9f4a249..00000000000 --- a/packages/protocol-dashboard/src/utils/performWithFallback.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Tries to run `work`, falls back to `fallback` if `work` throws. - * - * @param work - * @param fallback - */ -export const performWithFallback = async ( - work: () => Promise, - fallback: () => Promise -): Promise => { - try { - const res = await work() - return res - } catch (e) { - console.error(`Call failed, falling back. ${e.message}`) - } - - try { - const fall = await fallback() - return fall - } catch (e) { - console.error(`Fallback failed ${e.message}`) - throw e - } -}