diff --git a/apps/admin-x-framework/src/hooks/use-tinybird-query.ts b/apps/admin-x-framework/src/hooks/use-tinybird-query.ts index a08091bea4e..1753e856cf0 100644 --- a/apps/admin-x-framework/src/hooks/use-tinybird-query.ts +++ b/apps/admin-x-framework/src/hooks/use-tinybird-query.ts @@ -12,11 +12,11 @@ export interface UseTinybirdQueryOptions { // Wrapper around Tinybird's useQuery hook that handles the token loading state export const useTinybirdQuery = (options: UseTinybirdQueryOptions) => { - const tokenQuery = useTinybirdToken(); const {statsConfig, endpoint, params, enabled = true} = options; - const shouldQuery = enabled && statsConfig && endpoint; - const endpointUrl = shouldQuery ? getStatEndpointUrl(statsConfig, endpoint) : undefined; + const shouldQuery = Boolean(enabled && statsConfig && endpoint); + const tokenQuery = useTinybirdToken({enabled: shouldQuery}); + const endpointUrl = shouldQuery && statsConfig ? getStatEndpointUrl(statsConfig, endpoint) : undefined; // Set the endpoint to undefined if: // - Token is not loaded (prevents 403 errors) @@ -25,14 +25,14 @@ export const useTinybirdQuery = (options: UseTinybirdQueryOptions) => { // - No endpoint specified const {data, meta, loading, error} = useQuery({ endpoint: (!tokenQuery.isLoading && tokenQuery.token && shouldQuery) ? endpointUrl : undefined, - token: tokenQuery.token, + token: shouldQuery ? tokenQuery.token : undefined, params: params }); return { - data, - meta, - loading: tokenQuery.isLoading || loading, - error: error ?? tokenQuery.error + data: shouldQuery ? data : null, + meta: shouldQuery ? meta : null, + loading: shouldQuery ? tokenQuery.isLoading || loading : false, + error: shouldQuery ? error ?? tokenQuery.error : null }; }; diff --git a/apps/admin-x-framework/test/unit/hooks/use-active-visitors.test.ts b/apps/admin-x-framework/test/unit/hooks/use-active-visitors.test.ts index 31775a0f04c..ebb0de83fac 100644 --- a/apps/admin-x-framework/test/unit/hooks/use-active-visitors.test.ts +++ b/apps/admin-x-framework/test/unit/hooks/use-active-visitors.test.ts @@ -25,6 +25,11 @@ import {useTinybirdToken} from '../../../src/hooks/use-tinybird-token'; const mockUseQuery = vi.mocked(useQuery); const mockGetStatEndpointUrl = vi.mocked(getStatEndpointUrl); const mockUseTinybirdToken = vi.mocked(useTinybirdToken); +const statsConfig = { + id: 'test-site-id', + endpoint: 'https://api.test.com', + token: 'test-token' +}; describe('useActiveVisitors', () => { let queryClient: QueryClient; @@ -66,7 +71,7 @@ describe('useActiveVisitors', () => { }); it('returns initial state when enabled is true', () => { - const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + const {result} = renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); expect(result.current).toEqual({ activeVisitors: 0, @@ -97,7 +102,7 @@ describe('useActiveVisitors', () => { refresh: vi.fn() }); - const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + const {result} = renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); // Should show loading on initial load when no lastKnownCount exists expect(result.current.isLoading).toBe(true); @@ -117,7 +122,7 @@ describe('useActiveVisitors', () => { refresh: vi.fn() }); - const {result, rerender} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + const {result, rerender} = renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); expect(result.current.activeVisitors).toBe(25); // Second render with loading but data should not show loading @@ -150,7 +155,7 @@ describe('useActiveVisitors', () => { refresh: vi.fn() }); - const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + const {result} = renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); expect(result.current.activeVisitors).toBe(42); expect(result.current.isLoading).toBe(false); @@ -169,37 +174,31 @@ describe('useActiveVisitors', () => { refresh: vi.fn() }); - const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + const {result} = renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); expect(result.current.error).toBe(mockError); }); it('calls getStatEndpointUrl with correct parameters and uses tinybirdToken', () => { - const statsConfig = { - id: 'test-site-id', - endpoint: 'https://api.test.com', - token: 'test-token' - }; - renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); expect(mockGetStatEndpointUrl).toHaveBeenCalledWith(statsConfig, 'api_active_visitors'); expect(mockUseTinybirdToken).toHaveBeenCalled(); }); - it('calls useTinybirdQuery with undefined endpoint when no statsConfig and uses tinybirdToken', () => { + it('calls useTinybirdQuery with undefined endpoint and token when no statsConfig', () => { renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); expect(mockUseQuery).toHaveBeenCalledWith( expect.objectContaining({ endpoint: undefined, - token: 'mock-token', + token: undefined, params: expect.objectContaining({ site_uuid: '' }) }) ); - expect(mockUseTinybirdToken).toHaveBeenCalled(); + expect(mockUseTinybirdToken).toHaveBeenCalledWith({enabled: false}); }); it('sets up 60-second interval when enabled', () => { @@ -273,12 +272,6 @@ describe('useActiveVisitors', () => { }); it('uses statsConfig for site_uuid', () => { - const statsConfig = { - id: 'test-site-id', - endpoint: 'https://api.test.com', - token: 'test-token' - }; - renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); expect(mockUseQuery).toHaveBeenCalledWith( @@ -315,7 +308,7 @@ describe('useActiveVisitors', () => { refresh: vi.fn() }); - const {result, rerender} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + const {result, rerender} = renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); expect(result.current.activeVisitors).toBe(25); // Simulate refresh with loading state but no new data @@ -395,7 +388,7 @@ describe('useActiveVisitors', () => { refresh: vi.fn() }); - const {result, rerender} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + const {result, rerender} = renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); expect(result.current.activeVisitors).toBe(0); // Provide valid data @@ -439,7 +432,7 @@ describe('useActiveVisitors', () => { }; const {rerender} = renderHook( - ({statsConfig}) => useActiveVisitors({statsConfig, enabled: true}), + ({statsConfig: currentStatsConfig}) => useActiveVisitors({statsConfig: currentStatsConfig, enabled: true}), {initialProps: {statsConfig: initialStatsConfig}, wrapper} ); @@ -473,7 +466,7 @@ describe('useActiveVisitors', () => { }); const {result, rerender} = renderHook( - ({enabled}) => useActiveVisitors({enabled}), + ({enabled}) => useActiveVisitors({statsConfig, enabled}), {initialProps: {enabled: true}, wrapper} ); @@ -539,7 +532,7 @@ describe('useActiveVisitors', () => { mockUseQuery.mockClear(); // Render the hook - renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); // EXPECTED: useQuery should be called with undefined endpoint when token is loading // This prevents HTTP requests by disabling the SWR query entirely @@ -564,7 +557,7 @@ describe('useActiveVisitors', () => { mockUseQuery.mockClear(); // Render the hook - renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); // Should call useQuery with the valid token expect(mockUseQuery).toHaveBeenCalledWith( @@ -584,7 +577,7 @@ describe('useActiveVisitors', () => { }); // Render the hook - const {rerender} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + const {rerender} = renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); // Verify first call with undefined token (due to tokenLoading: true) expect(mockUseQuery).toHaveBeenLastCalledWith( @@ -636,7 +629,7 @@ describe('useActiveVisitors', () => { refresh: vi.fn() }); - const {result} = renderHook(() => useActiveVisitors({enabled: true}), {wrapper}); + const {result} = renderHook(() => useActiveVisitors({statsConfig, enabled: true}), {wrapper}); // Should show loading because token is loading and no lastKnownCount expect(result.current.isLoading).toBe(true); diff --git a/apps/admin-x-framework/test/unit/hooks/use-tinybird-query.test.ts b/apps/admin-x-framework/test/unit/hooks/use-tinybird-query.test.ts index f23fc6af8bf..221c4c24e3e 100644 --- a/apps/admin-x-framework/test/unit/hooks/use-tinybird-query.test.ts +++ b/apps/admin-x-framework/test/unit/hooks/use-tinybird-query.test.ts @@ -85,6 +85,58 @@ describe('useTinybirdQuery', () => { })); }); + it('should not fetch a token or query Tinybird when disabled', () => { + const mockError = new Error('Token error'); + mockUseTinybirdToken.mockReturnValue({ + token: 'mock-token', + isLoading: true, + error: mockError, + refetch: vi.fn() + }); + mockUseQuery.mockReturnValue({ + data: [{visits: 1}], + loading: true, + error: 'Query error', + meta: [{name: 'visits'}], + statistics: null, + endpoint: 'https://api.example.com/test', + token: 'mock-token', + refresh: vi.fn() + }); + + const {result} = renderHook(() => useTinybirdQuery({ + statsConfig: {id: '123'}, + endpoint: 'test', + params: {}, + enabled: false + }), {wrapper}); + + expect(mockUseTinybirdToken).toHaveBeenCalledWith({enabled: false}); + expect(mockGetStatEndpointUrl).not.toHaveBeenCalled(); + expect(mockUseQuery).toHaveBeenCalledWith(expect.objectContaining({ + endpoint: undefined, + token: undefined + })); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBe(null); + expect(result.current.data).toBe(null); + expect(result.current.meta).toBe(null); + }); + + it('should not fetch a token or query Tinybird without statsConfig', () => { + renderHook(() => useTinybirdQuery({ + endpoint: 'test', + params: {} + }), {wrapper}); + + expect(mockUseTinybirdToken).toHaveBeenCalledWith({enabled: false}); + expect(mockGetStatEndpointUrl).not.toHaveBeenCalled(); + expect(mockUseQuery).toHaveBeenCalledWith(expect.objectContaining({ + endpoint: undefined, + token: undefined + })); + }); + it('should call useQuery with the correct token', () => { mockUseTinybirdToken.mockReturnValue({ token: 'mock-token', diff --git a/apps/posts/src/providers/post-analytics-context.tsx b/apps/posts/src/providers/post-analytics-context.tsx index 71b5a94e4bb..19389e670f8 100644 --- a/apps/posts/src/providers/post-analytics-context.tsx +++ b/apps/posts/src/providers/post-analytics-context.tsx @@ -4,6 +4,7 @@ import {ReactNode, createContext, useContext, useState} from 'react'; import {STATS_RANGES} from '@src/utils/constants'; import {Setting, useBrowseSettings} from '@tryghost/admin-x-framework/api/settings'; import {StatsConfig, useTinybirdToken} from '@tryghost/admin-x-framework'; +import {useAppContext} from '@src/providers/posts-app-context'; import {useBrowseSite} from '@tryghost/admin-x-framework/api/site'; import {useParams} from '@tryghost/admin-x-framework'; @@ -62,6 +63,7 @@ export const useGlobalData = () => { }; const PostAnalyticsProvider = ({children}: { children: ReactNode }) => { + const {appSettings} = useAppContext(); const {postId} = useParams(); // Validate that postId exists - the app cannot function without it @@ -76,7 +78,8 @@ const PostAnalyticsProvider = ({children}: { children: ReactNode }) => { // Only fetch Tinybird token if stats config is present const hasStatsConfig = Boolean(config.data?.config?.stats); - const tinybirdTokenQuery = useTinybirdToken({enabled: hasStatsConfig}); + const shouldLoadTinybirdToken = hasStatsConfig && appSettings?.analytics?.webAnalytics === true; + const tinybirdTokenQuery = useTinybirdToken({enabled: shouldLoadTinybirdToken}); // Fetch post data with all required includes const {data: {posts: [post]} = {posts: []}, isLoading: isPostLoading} = useBrowsePosts({ @@ -89,12 +92,12 @@ const PostAnalyticsProvider = ({children}: { children: ReactNode }) => { // Check for errors in the ghost requests const ghostRequests = [config, site, settings]; const ghostError = ghostRequests.map(request => request.error).find(Boolean); - const tinybirdError = hasStatsConfig ? tinybirdTokenQuery.error : null; + const tinybirdError = shouldLoadTinybirdToken ? tinybirdTokenQuery.error : null; const error = ghostError || tinybirdError; // Check loading states const isGhostLoading = ghostRequests.some(request => request.isLoading); - const isTinybirdLoading = hasStatsConfig ? tinybirdTokenQuery.isLoading : false; + const isTinybirdLoading = shouldLoadTinybirdToken ? tinybirdTokenQuery.isLoading : false; const isLoading = isGhostLoading || isTinybirdLoading; if (error) { diff --git a/apps/posts/src/views/PostAnalytics/Overview/overview.tsx b/apps/posts/src/views/PostAnalytics/Overview/overview.tsx index 6a9f85df5a6..a4a90afaa62 100644 --- a/apps/posts/src/views/PostAnalytics/Overview/overview.tsx +++ b/apps/posts/src/views/PostAnalytics/Overview/overview.tsx @@ -23,6 +23,7 @@ const Overview: React.FC = () => { const {totals, isLoading: isTotalsLoading, currencySymbol} = usePostReferrers(postId); const {appSettings} = useAppContext(); const {emailTrackClicks: emailTrackClicksEnabled, emailTrackOpens: emailTrackOpensEnabled} = appSettings?.analytics || {}; + const webAnalyticsEnabled = appSettings?.analytics?.webAnalytics === true; // Calculate chart range based on days between today and post publication date const chartRange = useMemo(() => { @@ -57,8 +58,9 @@ const Overview: React.FC = () => { const {data: chartData, loading: chartLoading} = useTinybirdQuery({ endpoint: 'api_kpis', - statsConfig: statsConfig || {id: ''}, - params: params + statsConfig, + params: params, + enabled: webAnalyticsEnabled }); // Calculate total visitors as a number for WebOverview component @@ -87,8 +89,9 @@ const Overview: React.FC = () => { // Get sources data const {data: sourcesData, loading: isSourcesLoading} = useTinybirdQuery({ endpoint: 'api_top_sources', - statsConfig: statsConfig || {id: ''}, - params: params + statsConfig, + params: params, + enabled: webAnalyticsEnabled }); const kpiIsLoading = isConfigLoading || isTotalsLoading || isPostLoading || chartLoading; @@ -96,16 +99,16 @@ const Overview: React.FC = () => { // Use the utility function from admin-x-framework const showNewsletterSection = hasBeenEmailed(post as Post) && emailTrackOpensEnabled && emailTrackClicksEnabled; - const showWebSection = !post?.email_only && appSettings?.analytics.webAnalytics; + const showWebSection = !post?.email_only && webAnalyticsEnabled; const showGrowthSection = appSettings?.analytics.membersTrackSources; // Redirect to Growth tab if this is a published-only post with web analytics disabled // Only redirect if Growth section is available useEffect(() => { - if (!isPostLoading && post && isPublishedOnly(post as Post) && !appSettings?.analytics.webAnalytics && showGrowthSection) { + if (!isPostLoading && post && isPublishedOnly(post as Post) && !webAnalyticsEnabled && showGrowthSection) { navigate(`/posts/analytics/${postId}/growth`, {replace: true}); } - }, [isPostLoading, post, appSettings?.analytics.webAnalytics, navigate, postId, showGrowthSection]); + }, [isPostLoading, post, webAnalyticsEnabled, navigate, postId, showGrowthSection]); // First we have to wait for the post to be loaded to determine what sections (web, newsletter etc.) should be displayed if (isPostLoading) { diff --git a/apps/posts/src/views/PostAnalytics/Web/web.tsx b/apps/posts/src/views/PostAnalytics/Web/web.tsx index bd7df209972..4b2234d9d6b 100644 --- a/apps/posts/src/views/PostAnalytics/Web/web.tsx +++ b/apps/posts/src/views/PostAnalytics/Web/web.tsx @@ -14,6 +14,7 @@ import {createFilter} from '@tryghost/shade/patterns'; import {formatQueryDate, getRangeDates, getRangeForStartDate} from '@tryghost/shade/app'; import {getAudienceFromFilterValues, getAudienceQueryParam} from '@src/utils/audience'; import {getPeriodText} from '@src/utils/chart-helpers'; +import {useAppContext} from '@src/providers/posts-app-context'; import {useCallback, useEffect, useMemo, useRef} from 'react'; import {useFilterParams} from '@src/hooks/use-filter-params'; import {useGlobalData} from '@src/providers/post-analytics-context'; @@ -31,6 +32,8 @@ const Web: React.FC = () => { const navigate = useNavigate(); const {postId} = useParams(); const {statsConfig, isLoading: isConfigLoading, range, data: globalData, post, isPostLoading} = useGlobalData(); + const {appSettings} = useAppContext(); + const webAnalyticsEnabled = appSettings?.analytics?.webAnalytics === true; const containerRef = useRef(null); // Use URL-synced filter state for bookmarking and sharing @@ -143,22 +146,25 @@ const Web: React.FC = () => { // Get web kpi data const {data: kpiData, loading: isKpisLoading} = useTinybirdQuery({ endpoint: 'api_kpis', - statsConfig: statsConfig || {id: ''}, - params: params + statsConfig, + params: params, + enabled: webAnalyticsEnabled }); // Get locations data const {data: locationsData, loading: isLocationsLoading} = useTinybirdQuery({ endpoint: 'api_top_locations', - statsConfig: statsConfig || {id: ''}, - params: params + statsConfig, + params: params, + enabled: webAnalyticsEnabled }); // Get sources data const {data: sourcesData, loading: isSourcesLoading} = useTinybirdQuery({ endpoint: 'api_top_sources', - statsConfig: statsConfig || {id: ''}, - params: params + statsConfig, + params: params, + enabled: webAnalyticsEnabled }); // Calculate total visits for percentage calculation diff --git a/apps/posts/src/views/PostAnalytics/components/stats-filter.tsx b/apps/posts/src/views/PostAnalytics/components/stats-filter.tsx index 81f002f19ec..6ac896ed16f 100644 --- a/apps/posts/src/views/PostAnalytics/components/stats-filter.tsx +++ b/apps/posts/src/views/PostAnalytics/components/stats-filter.tsx @@ -204,6 +204,7 @@ function StatsFilter({filters, onChange, ...props}: StatsFilterProps) { const {appSettings} = useAppContext(); const {post} = useGlobalData(); const postUuid = post?.uuid; + const webAnalyticsEnabled = appSettings?.analytics?.webAnalytics === true; // Track which filter field is currently being selected (lazy loading) const [activeFilterField, setActiveFilterField] = useState(null); @@ -248,14 +249,14 @@ function StatsFilter({filters, onChange, ...props}: StatsFilterProps) { // Fetch options for all Tinybird-backed fields using the generic hook // Options are contextual - filtered based on currently applied filters and post_uuid // Lazy loading: only fetch when field is active or has applied filter - const {options: utmSourceOptions, loading: utmSourceLoading} = useTinybirdFilterOptions('utm_source', filters, postUuid, {enabled: shouldFetchOptions('utm_source')}); - const {options: utmMediumOptions, loading: utmMediumLoading} = useTinybirdFilterOptions('utm_medium', filters, postUuid, {enabled: shouldFetchOptions('utm_medium')}); - const {options: utmCampaignOptions, loading: utmCampaignLoading} = useTinybirdFilterOptions('utm_campaign', filters, postUuid, {enabled: shouldFetchOptions('utm_campaign')}); - const {options: utmContentOptions, loading: utmContentLoading} = useTinybirdFilterOptions('utm_content', filters, postUuid, {enabled: shouldFetchOptions('utm_content')}); - const {options: utmTermOptions, loading: utmTermLoading} = useTinybirdFilterOptions('utm_term', filters, postUuid, {enabled: shouldFetchOptions('utm_term')}); - const {options: sourceOptions, loading: sourceLoading} = useTinybirdFilterOptions('source', filters, postUuid, {enabled: shouldFetchOptions('source')}); - const {options: deviceOptions, loading: deviceLoading} = useTinybirdFilterOptions('device', filters, postUuid, {enabled: shouldFetchOptions('device')}); - const {options: locationOptions, loading: locationLoading} = useTinybirdFilterOptions('location', filters, postUuid, {enabled: shouldFetchOptions('location')}); + const {options: utmSourceOptions, loading: utmSourceLoading} = useTinybirdFilterOptions('utm_source', filters, postUuid, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_source')}); + const {options: utmMediumOptions, loading: utmMediumLoading} = useTinybirdFilterOptions('utm_medium', filters, postUuid, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_medium')}); + const {options: utmCampaignOptions, loading: utmCampaignLoading} = useTinybirdFilterOptions('utm_campaign', filters, postUuid, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_campaign')}); + const {options: utmContentOptions, loading: utmContentLoading} = useTinybirdFilterOptions('utm_content', filters, postUuid, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_content')}); + const {options: utmTermOptions, loading: utmTermLoading} = useTinybirdFilterOptions('utm_term', filters, postUuid, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_term')}); + const {options: sourceOptions, loading: sourceLoading} = useTinybirdFilterOptions('source', filters, postUuid, {enabled: webAnalyticsEnabled && shouldFetchOptions('source')}); + const {options: deviceOptions, loading: deviceLoading} = useTinybirdFilterOptions('device', filters, postUuid, {enabled: webAnalyticsEnabled && shouldFetchOptions('device')}); + const {options: locationOptions, loading: locationLoading} = useTinybirdFilterOptions('location', filters, postUuid, {enabled: webAnalyticsEnabled && shouldFetchOptions('location')}); // Note: Only 'is' operator supported - Tinybird pipes only support exact match const supportedOperators = useMemo(() => [ diff --git a/apps/stats/src/providers/global-data-provider.tsx b/apps/stats/src/providers/global-data-provider.tsx index 99a08dbd978..a9cd3000f7c 100644 --- a/apps/stats/src/providers/global-data-provider.tsx +++ b/apps/stats/src/providers/global-data-provider.tsx @@ -2,7 +2,7 @@ import {Config, useBrowseConfig} from '@tryghost/admin-x-framework/api/config'; import {ReactNode, createContext, useContext, useState} from 'react'; import {STATS_DEFAULT_RANGE_KEY, STATS_RANGE_OPTIONS} from '@src/utils/constants'; import {Setting, useBrowseSettings} from '@tryghost/admin-x-framework/api/settings'; -import {StatsConfig, useTinybirdToken} from '@tryghost/admin-x-framework'; +import {StatsConfig, useAppContext, useTinybirdToken} from '@tryghost/admin-x-framework'; import {useBrowseSite} from '@tryghost/admin-x-framework/api/site'; interface GlobalData { @@ -33,25 +33,27 @@ export const useGlobalData = () => { }; const GlobalDataProvider = ({children}: { children: ReactNode }) => { + const {appSettings} = useAppContext(); const settings = useBrowseSettings(); const site = useBrowseSite(); const config = useBrowseConfig(); // config.data is ConfigResponseType which has shape { config: Config } const configData = config.data?.config; const hasStatsConfig = Boolean(configData?.stats); - const tinybirdTokenQuery = useTinybirdToken({enabled: hasStatsConfig}); + const shouldLoadTinybirdToken = hasStatsConfig && appSettings?.analytics?.webAnalytics === true; + const tinybirdTokenQuery = useTinybirdToken({enabled: shouldLoadTinybirdToken}); const [range, setRange] = useState(STATS_RANGE_OPTIONS[STATS_DEFAULT_RANGE_KEY].value); const [selectedNewsletterId, setSelectedNewsletterId] = useState(null); // Check for errors in the main requests const ghostRequests = [config, settings, site]; const ghostError = ghostRequests.map(request => request.error).find(Boolean); - const tinybirdError = hasStatsConfig ? tinybirdTokenQuery.error : null; + const tinybirdError = shouldLoadTinybirdToken ? tinybirdTokenQuery.error : null; const error = ghostError || tinybirdError; // Check loading states const isGhostLoading = ghostRequests.some(request => request.isLoading); - const isTinybirdLoading = hasStatsConfig ? tinybirdTokenQuery.isLoading : false; + const isTinybirdLoading = shouldLoadTinybirdToken ? tinybirdTokenQuery.isLoading : false; const isLoading = isGhostLoading || isTinybirdLoading; if (error) { diff --git a/apps/stats/src/views/Stats/Overview/overview.tsx b/apps/stats/src/views/Stats/Overview/overview.tsx index ea7b120016c..f91de7db427 100644 --- a/apps/stats/src/views/Stats/Overview/overview.tsx +++ b/apps/stats/src/views/Stats/Overview/overview.tsx @@ -71,6 +71,7 @@ type GrowthChartDataItem = { const Overview: React.FC = () => { const {appSettings} = useAppContext(); const {statsConfig, isLoading: isConfigLoading, range} = useGlobalData(); + const webAnalyticsEnabled = appSettings?.analytics?.webAnalytics === true; const {startDate, endDate, timezone} = getRangeDates(range); const {isLoading: isGrowthStatsLoading, chartData: growthChartData, totals: growthTotals, currencySymbol} = useGrowthStats(range); const {data: latestPostStats, isLoading: isLatestPostLoading} = useLatestPostStats(); @@ -80,7 +81,8 @@ const Overview: React.FC = () => { date_to: formatQueryDate(endDate), limit: '5', timezone - } + }, + enabled: webAnalyticsEnabled }); /* Get visitors @@ -96,7 +98,8 @@ const Overview: React.FC = () => { const {data: visitorsData, loading: isVisitorsLoading} = useTinybirdQuery({ endpoint: 'api_kpis', statsConfig, - params: visitorsParams + params: visitorsParams, + enabled: webAnalyticsEnabled }); const visitorsChartData = useMemo(() => { diff --git a/apps/stats/src/views/Stats/Web/web.tsx b/apps/stats/src/views/Stats/Web/web.tsx index 308e4efcbcd..da88b2475eb 100644 --- a/apps/stats/src/views/Stats/Web/web.tsx +++ b/apps/stats/src/views/Stats/Web/web.tsx @@ -57,6 +57,7 @@ const Web: React.FC = () => { const {statsConfig, isLoading: isConfigLoading, range, data} = useGlobalData(); const {startDate, endDate, timezone} = getRangeDates(range); const {appSettings} = useAppContext(); + const webAnalyticsEnabled = appSettings?.analytics?.webAnalytics === true; const containerRef = useRef(null); @@ -155,21 +156,24 @@ const Web: React.FC = () => { const {data: kpiData, loading: kpiLoading} = useTinybirdQuery({ endpoint: 'api_kpis', statsConfig, - params + params, + enabled: webAnalyticsEnabled }); // Get top sources data const {data: sourcesData, loading: isSourcesLoading} = useTinybirdQuery({ endpoint: 'api_top_sources', statsConfig, - params + params, + enabled: webAnalyticsEnabled }); // Get top locations data const {data: locationsData, loading: isLocationsLoading} = useTinybirdQuery({ endpoint: 'api_top_locations', statsConfig, - params + params, + enabled: webAnalyticsEnabled }); // Get total visitors for table @@ -178,7 +182,7 @@ const Web: React.FC = () => { // Calculate combined loading state const isPageLoading = isConfigLoading; - if (!appSettings?.analytics.webAnalytics) { + if (!webAnalyticsEnabled) { return ( ); diff --git a/apps/stats/src/views/Stats/components/stats-filter.tsx b/apps/stats/src/views/Stats/components/stats-filter.tsx index 72086ce490e..674199dc2c4 100644 --- a/apps/stats/src/views/Stats/components/stats-filter.tsx +++ b/apps/stats/src/views/Stats/components/stats-filter.tsx @@ -277,6 +277,7 @@ const usePostOptions = (currentFilters: Filter[] = [], config: UsePostOptionsCon function StatsFilter({filters, onChange, ...props}: StatsFilterProps) { const {appSettings} = useAppContext(); + const webAnalyticsEnabled = appSettings?.analytics?.webAnalytics === true; // Track which filter field is currently being selected (lazy loading) const [activeFilterField, setActiveFilterField] = useState(null); @@ -321,17 +322,17 @@ function StatsFilter({filters, onChange, ...props}: StatsFilterProps) { // Fetch options for all Tinybird-backed fields using the generic hook // Options are contextual - filtered based on currently applied filters // Lazy loading: only fetch when field is active or has applied filter - const {options: utmSourceOptions, loading: utmSourceLoading} = useTinybirdFilterOptions('utm_source', filters, {enabled: shouldFetchOptions('utm_source')}); - const {options: utmMediumOptions, loading: utmMediumLoading} = useTinybirdFilterOptions('utm_medium', filters, {enabled: shouldFetchOptions('utm_medium')}); - const {options: utmCampaignOptions, loading: utmCampaignLoading} = useTinybirdFilterOptions('utm_campaign', filters, {enabled: shouldFetchOptions('utm_campaign')}); - const {options: utmContentOptions, loading: utmContentLoading} = useTinybirdFilterOptions('utm_content', filters, {enabled: shouldFetchOptions('utm_content')}); - const {options: utmTermOptions, loading: utmTermLoading} = useTinybirdFilterOptions('utm_term', filters, {enabled: shouldFetchOptions('utm_term')}); - const {options: sourceOptions, loading: sourceLoading} = useTinybirdFilterOptions('source', filters, {enabled: shouldFetchOptions('source')}); - const {options: deviceOptions, loading: deviceLoading} = useTinybirdFilterOptions('device', filters, {enabled: shouldFetchOptions('device')}); - const {options: locationOptions, loading: locationLoading} = useTinybirdFilterOptions('location', filters, {enabled: shouldFetchOptions('location')}); + const {options: utmSourceOptions, loading: utmSourceLoading} = useTinybirdFilterOptions('utm_source', filters, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_source')}); + const {options: utmMediumOptions, loading: utmMediumLoading} = useTinybirdFilterOptions('utm_medium', filters, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_medium')}); + const {options: utmCampaignOptions, loading: utmCampaignLoading} = useTinybirdFilterOptions('utm_campaign', filters, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_campaign')}); + const {options: utmContentOptions, loading: utmContentLoading} = useTinybirdFilterOptions('utm_content', filters, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_content')}); + const {options: utmTermOptions, loading: utmTermLoading} = useTinybirdFilterOptions('utm_term', filters, {enabled: webAnalyticsEnabled && shouldFetchOptions('utm_term')}); + const {options: sourceOptions, loading: sourceLoading} = useTinybirdFilterOptions('source', filters, {enabled: webAnalyticsEnabled && shouldFetchOptions('source')}); + const {options: deviceOptions, loading: deviceLoading} = useTinybirdFilterOptions('device', filters, {enabled: webAnalyticsEnabled && shouldFetchOptions('device')}); + const {options: locationOptions, loading: locationLoading} = useTinybirdFilterOptions('location', filters, {enabled: webAnalyticsEnabled && shouldFetchOptions('location')}); // Fetch options for posts - data is contextual based on current filters - const {options: postOptions, loading: postLoading} = usePostOptions(filters, {enabled: shouldFetchOptions('post')}); + const {options: postOptions, loading: postLoading} = usePostOptions(filters, {enabled: webAnalyticsEnabled && shouldFetchOptions('post')}); // Note: Only 'is' operator supported - Tinybird pipes only support exact match const supportedOperators = useMemo(() => [