diff --git a/studio/__mocks__/hooks.js b/studio/__mocks__/hooks.js
index bfacf3748e5..e6d89720b13 100644
--- a/studio/__mocks__/hooks.js
+++ b/studio/__mocks__/hooks.js
@@ -16,3 +16,5 @@ window.matchMedia = jest.fn().mockReturnValue({
removeEventListener: jest.fn(),
})
export const StoreProvider = StoreProvider
+
+export const withAuth = jest.fn()
\ No newline at end of file
diff --git a/studio/__mocks__/lib/common/fetch.js b/studio/__mocks__/lib/common/fetch.js
index b957643e53b..ab231111a61 100644
--- a/studio/__mocks__/lib/common/fetch.js
+++ b/studio/__mocks__/lib/common/fetch.js
@@ -1 +1,2 @@
export const get = jest.fn()
+export const post = jest.fn()
diff --git a/studio/components/interfaces/Reports/Reports.constants.ts b/studio/components/interfaces/Reports/Reports.constants.ts
index 797ee40d311..59ef87fd261 100644
--- a/studio/components/interfaces/Reports/Reports.constants.ts
+++ b/studio/components/interfaces/Reports/Reports.constants.ts
@@ -180,6 +180,18 @@ order by
cumulativeUsers: {
queryType: 'db',
sql: `
+
+select
+ date_trunc('day', u.created_at) as timestamp,
+ sum(count(u.id)) over (order by date_trunc('day', u.created_at)) as count
+from auth.users as u
+group by timestamp
+order by timestamp desc
+ `,
+ },
+ newUsers: {
+ queryType: 'db',
+ sql: `
select
date_trunc('day', u.created_at) as timestamp,
count(u.id) as count
@@ -189,24 +201,6 @@ group by
timestamp
order by
timestamp desc
- `,
- },
- newUsers: {
- queryType: 'logs',
- sql: `
-select
- TIMESTAMP_TRUNC(f.timestamp, DAY) as timestamp,
- count(id) as count
-from
-gotrue_logs f
- cross join unnest(f.metadata) as m
-where f.timestamp > "2022-11-20"
-and m.project = "tzljvicuuxegpzwuwife"
-and JSON_VALUE(f.event_message, "$.path") = "/signup"
-and JSON_VALUE(f.event_message, "$.auth_event.actor_id") is not null
-and JSON_VALUE(f.event_message, "$.auth_event.action") != "user_repeated_signup"
-group by
- timestamp
`,
},
},
diff --git a/studio/components/interfaces/Reports/renderers/AuthRenderers.tsx b/studio/components/interfaces/Reports/renderers/AuthRenderers.tsx
index f719a91f1da..f85ddabeece 100644
--- a/studio/components/interfaces/Reports/renderers/AuthRenderers.tsx
+++ b/studio/components/interfaces/Reports/renderers/AuthRenderers.tsx
@@ -1,5 +1,4 @@
import { BarChart } from 'components/to-be-cleaned/Charts/ChartRenderer'
-import { first } from 'lodash'
import { DATETIME_FORMAT } from '../Reports.constants'
import { ReportWidgetProps } from '../ReportWidget'
@@ -23,27 +22,6 @@ export const renderCumulativeUsers = (
)
}
-export const renderDailyActiveUsers = (
- props: ReportWidgetProps<{
- count: number
- timestamp: string
- }>
-) => {
- return (
- <>
-
-
-
- >
- )
-}
export const renderNewUsers = (
props: ReportWidgetProps<{
@@ -51,18 +29,37 @@ export const renderNewUsers = (
timestamp: string
}>
) => {
+ const sum = (props.data || []).reduce((acc, datum) => datum.count + acc, 0)
return (
- <>
-
-
-
- >
+
+
+
+ )
+}
+
+export const renderDailyActiveUsers = (
+ props: ReportWidgetProps<{
+ count: number
+ timestamp: string
+ }>
+) => {
+ return (
+
+
+
)
}
diff --git a/studio/hooks/analytics/useDbQuery.tsx b/studio/hooks/analytics/useDbQuery.tsx
index 979f30a1e5d..9dbbf902440 100644
--- a/studio/hooks/analytics/useDbQuery.tsx
+++ b/studio/hooks/analytics/useDbQuery.tsx
@@ -14,7 +14,6 @@ const useDbQuery: UseDbQuery = (sql, params = DEFAULT_QUERY_PARAMS) => {
const { meta } = useStore()
const resolvedSql = typeof sql === 'function' ? sql(params) : sql
- console.log('useDbQuery', 'sql', resolvedSql)
const {
data,
error: swrError,
@@ -23,8 +22,6 @@ const useDbQuery: UseDbQuery = (sql, params = DEFAULT_QUERY_PARAMS) => {
} = useSWR(resolvedSql, async () => await meta.query(resolvedSql), {
revalidateOnFocus: false,
})
- console.log('useDbQuery', 'data', data)
- console.log('useDbQuery', 'isLoading', isLoading)
const error = swrError || (typeof data === 'object' ? data?.error : '')
return [{ error, data, isLoading, params }, { runQuery: () => mutate() }]
diff --git a/studio/pages/project/[ref]/reports/api-bots.tsx b/studio/pages/project/[ref]/reports/api-bots.tsx
index 5f4198ac55a..5da674c7a09 100644
--- a/studio/pages/project/[ref]/reports/api-bots.tsx
+++ b/studio/pages/project/[ref]/reports/api-bots.tsx
@@ -9,7 +9,7 @@ import { PRESET_CONFIG } from 'components/interfaces/Reports/Reports.constants'
import { renderBotScores, renderUserAgents } from 'components/interfaces/Reports/renderers/ApiBotsRenderers'
import ReportWidget from 'components/interfaces/Reports/ReportWidget'
-export const ApiBotsPage: NextPageWithLayout = () => {
+export const ApiBotsReport: NextPageWithLayout = () => {
const router = useRouter()
const { ref } = router.query
const config = PRESET_CONFIG[Presets.API_BOTS]
@@ -28,7 +28,7 @@ export const ApiBotsPage: NextPageWithLayout = () => {
className="col-span-4 col-start-1"
title="Top User Agents"
description="The types of user agents using the API and their organization source."
- data={userAgents[0].logData}
+ data={userAgents[0].logData || []}
renderer={renderUserAgents}
/>
{
className="col-span-4 col-start-1"
title="Bot-like API Requests"
description="Suspicious bot-like requests, as flagged by Cloudflare"
- data={botScores[0].logData}
+ data={botScores[0].logData || []}
renderer={renderBotScores}
/>
@@ -45,6 +45,6 @@ export const ApiBotsPage: NextPageWithLayout = () => {
)
}
-ApiBotsPage.getLayout = (page) => {page}
+ApiBotsReport.getLayout = (page) => {page}
-export default observer(ApiBotsPage)
+export default observer(ApiBotsReport)
diff --git a/studio/pages/project/[ref]/reports/api-overview.tsx b/studio/pages/project/[ref]/reports/api-overview.tsx
index 7f2881877bb..ad9c4d2ecdc 100644
--- a/studio/pages/project/[ref]/reports/api-overview.tsx
+++ b/studio/pages/project/[ref]/reports/api-overview.tsx
@@ -13,7 +13,7 @@ import {
renderStatusCodesChart,
} from 'components/interfaces/Reports/renderers/OverviewRenderers'
-export const ApiOverviewPage: NextPageWithLayout = () => {
+export const ApiOverviewReport: NextPageWithLayout = () => {
const router = useRouter()
const { ref } = router.query
const config = PRESET_CONFIG[Presets.API_OVERVIEW]
@@ -31,7 +31,7 @@ export const ApiOverviewPage: NextPageWithLayout = () => {
className="col-span-4 col-start-1"
title="API Status Codes"
description="Distribution of API responses by status codes."
- data={statusCodes[0].logData}
+ data={statusCodes[0].logData || []}
renderer={renderStatusCodesChart}
/>
{
className="col-span-4 col-start-1"
title="Error Rate"
description="Percentage of API 5XX and 4XX error responses."
- data={errorRates[0].logData}
+ data={errorRates[0].logData || []}
renderer={renderErrorRateChart}
/>
{
className="col-span-4 col-start-1"
title="Slow API Requests"
description="Most frequently used requests, sorted by total querying time"
- data={requestPaths[0].logData}
+ data={requestPaths[0].logData || []}
renderer={renderRequestsPathsTable}
/>
@@ -57,6 +57,6 @@ export const ApiOverviewPage: NextPageWithLayout = () => {
)
}
-ApiOverviewPage.getLayout = (page) => {page}
+ApiOverviewReport.getLayout = (page) => {page}
-export default observer(ApiOverviewPage)
+export default observer(ApiOverviewReport)
diff --git a/studio/pages/project/[ref]/reports/auth.tsx b/studio/pages/project/[ref]/reports/auth.tsx
index 1d1557f2444..5e6c6781a79 100644
--- a/studio/pages/project/[ref]/reports/auth.tsx
+++ b/studio/pages/project/[ref]/reports/auth.tsx
@@ -13,7 +13,7 @@ import {
renderCumulativeUsers,
} from 'components/interfaces/Reports/renderers/AuthRenderers'
-export const ApiBotsPage: NextPageWithLayout = () => {
+export const AuthReport: NextPageWithLayout = () => {
const router = useRouter()
const { ref } = router.query
const config = PRESET_CONFIG[Presets.AUTH]
@@ -31,33 +31,33 @@ export const ApiBotsPage: NextPageWithLayout = () => {
params={cumulativeUsers[0].params}
className="col-span-2 col-start-1"
title="Users Over Time"
- description="Daily count of confirmed users"
+ description="Cumulative count of confirmed users"
data={cumulativeUsers[0].data!}
renderer={renderCumulativeUsers}
/>
+
-
)
}
-ApiBotsPage.getLayout = (page) => {page}
+AuthReport.getLayout = (page) => {page}
-export default observer(ApiBotsPage)
+export default observer(AuthReport)
diff --git a/studio/tests/components/Reports/PresetReport.test.js b/studio/tests/components/Reports/PresetReport.test.js
deleted file mode 100644
index 7813c376821..00000000000
--- a/studio/tests/components/Reports/PresetReport.test.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import dayjs from 'dayjs'
-import utc from 'dayjs/plugin/utc'
-
-dayjs.extend(utc)
-
-// mock the fetch function
-jest.mock('lib/common/fetch')
-import { get } from 'lib/common/fetch'
-
-// mock mobx
-jest.mock('mobx-react-lite')
-import { observer } from 'mobx-react-lite'
-observer.mockImplementation((v) => v)
-
-// mock the router
-jest.mock('next/router')
-import { useRouter } from 'next/router'
-const defaultRouterMock = () => {
- const router = jest.fn()
- router.query = {}
- router.push = jest.fn()
- router.pathname = 'logs/path'
- return router
-}
-useRouter.mockReturnValue(defaultRouterMock())
-// mock usage flags
-jest.mock('components/ui/Flag/Flag')
-import Flag from 'components/ui/Flag/Flag'
-Flag.mockImplementation(({ children }) => <>{children}>)
-jest.mock('hooks')
-import { useFlag } from 'hooks'
-useFlag.mockReturnValue(true)
-
-import { SWRConfig } from 'swr'
-jest.mock('components/interfaces/Reports/PresetReport')
-import PresetReport from 'components/interfaces/Reports/PresetReport'
-PresetReport.mockImplementation((props) => {
- const Comp = jest.requireActual('components/interfaces/Reports/PresetReport').default
- // wrap with SWR to reset the cache each time
- return (
- new Map(),
- shouldRetryOnError: false,
- }}
- >
-
-
- )
-})
-
-jest.mock('hooks')
-import { useProjectSubscription } from 'hooks'
-useProjectSubscription = jest.fn((ref) => ({
- subscription: {
- tier: {
- supabase_prod_id: 'tier_free',
- },
- },
-}))
-
-import { render, fireEvent, waitFor, screen, act } from '@testing-library/react'
-import { Presets } from 'components/interfaces/Reports/Reports.types'
-beforeEach(() => {
- // reset mocks between tests
- get.mockReset()
- useRouter.mockReset()
- useRouter.mockReturnValue(defaultRouterMock())
-})
-
-test('static elements', async () => {
- render()
- await screen.findByText(/Last 7 days/)
- await screen.findAllByText(/Refresh/)
-})
-
-test('changing date range triggers query refresh', async () => {
- render()
- await waitFor(() => expect(get).toBeCalled())
- get.mockReset()
- const refresh = await screen.findByText(/Refresh/)
- fireEvent.click(refresh)
- await waitFor(() => expect(get).toBeCalled())
-})
diff --git a/studio/tests/pages/projects/reports/product-reports.test.js b/studio/tests/pages/projects/reports/product-reports.test.js
new file mode 100644
index 00000000000..a30e33c1aea
--- /dev/null
+++ b/studio/tests/pages/projects/reports/product-reports.test.js
@@ -0,0 +1,55 @@
+import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc'
+dayjs.extend(utc)
+
+import { get, post } from 'lib/common/fetch'
+
+import { useRouter } from 'next/router'
+import { render } from '../../../helpers'
+import { fireEvent, waitFor, screen } from '@testing-library/react'
+import { ApiOverviewReport } from 'pages/project/[ref]/reports/api-overview'
+import { ApiBotsReport } from 'pages/project/[ref]/reports/api-bots'
+import { AuthReport } from 'pages/project/[ref]/reports/auth'
+
+beforeEach(() => {
+ // reset mocks between tests
+ get.mockReset()
+ useRouter.mockReset()
+ useRouter.mockReturnValue({
+ query: { ref: '123' },
+ })
+ get.mockImplementation(async (url) => {
+ return [{ data: [] }]
+ })
+ post.mockResolvedValue([])
+})
+
+describe.each([
+ { Page: ApiOverviewReport, contains: ['API Overview'] },
+ { Page: ApiBotsReport, contains: ['API Bots'] },
+ { Page: AuthReport, contains: ['Auth'] },
+])('$Page rendering', ({ Page, contains }) => {
+ test(`contains ${contains}`, async () => {
+ render()
+
+ contains.forEach((word) => {
+ expect(screen.findByText(word)).resolves.toBeTruthy()
+ })
+ })
+ test('static elements', async () => {
+ render()
+ await screen.findByText(/Last 7 days/)
+ await screen.findAllByText(/Refresh/)
+ })
+
+ test('changing date range triggers query refresh', async () => {
+ render()
+ await waitFor(() => expect(get).toBeCalled())
+ get.mockReset()
+ const refresh = await screen.findByText(/Refresh/)
+ fireEvent.click(refresh)
+
+ const calls = get.mock.calls.concat(post.mock.calls)
+ expect(calls.length).toBeGreaterThan(0)
+ })
+})