From 69205d6a30e00a094da25bddcf416075de58d4a6 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Wed, 7 May 2025 13:35:53 +0200 Subject: [PATCH] update auth charts --- .../components/ui/Charts/ChartHeader.tsx | 23 +- .../components/ui/Charts/ComposedChart.tsx | 22 +- .../ui/Charts/ComposedChart.utils.tsx | 2 +- .../ui/Charts/ComposedChartHandler.tsx | 8 +- apps/studio/data/reports/auth-charts.ts | 459 +++++++++++++++--- 5 files changed, 436 insertions(+), 78 deletions(-) diff --git a/apps/studio/components/ui/Charts/ChartHeader.tsx b/apps/studio/components/ui/Charts/ChartHeader.tsx index 7eabf60887e..2483346c649 100644 --- a/apps/studio/components/ui/Charts/ChartHeader.tsx +++ b/apps/studio/components/ui/Charts/ChartHeader.tsx @@ -1,5 +1,5 @@ import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' -import { Activity, BarChartIcon, GitCommitHorizontalIcon } from 'lucide-react' +import { Activity, BarChartIcon, GitCommitHorizontalIcon, InfoIcon } from 'lucide-react' export interface ChartHeaderProps { title?: string @@ -14,6 +14,7 @@ export interface ChartHeaderProps { onChartStyleChange?: (style: string) => void showMaxValue?: boolean setShowMaxValue?: (value: boolean) => void + titleTooltip?: string } const ChartHeader = ({ @@ -27,10 +28,24 @@ const ChartHeader = ({ onChartStyleChange, showMaxValue = false, setShowMaxValue, + titleTooltip, }: ChartHeaderProps) => { const chartTitle = ( -

- {title} +

+ {title}{' '} + {titleTooltip && ( + + + + + {titleTooltip} + + )}

) @@ -61,7 +76,7 @@ const ChartHeader = ({ return (
-
+
{title && chartTitle} {highlightedValue !== undefined && highlighted} {label} diff --git a/apps/studio/components/ui/Charts/ComposedChart.tsx b/apps/studio/components/ui/Charts/ComposedChart.tsx index e6612df02e3..868db2021ca 100644 --- a/apps/studio/components/ui/Charts/ComposedChart.tsx +++ b/apps/studio/components/ui/Charts/ComposedChart.tsx @@ -32,9 +32,9 @@ import { CustomTooltip, formatBytes, } from './ComposedChart.utils' -import { MultiAttribute } from './ComposedChartHandler' -import NoDataPlaceholder from './NoDataPlaceholder' import { ChartHighlight } from './useChartHighlight' +import NoDataPlaceholder from './NoDataPlaceholder' +import { MultiAttribute } from './ComposedChartHandler' export interface BarChartProps extends CommonChartProps { attributes: MultiAttribute[] @@ -56,6 +56,7 @@ export interface BarChartProps extends CommonChartProps { chartStyle?: string onChartStyleChange?: (style: string) => void updateDateRange: any + titleTooltip?: string } export default function ComposedChart({ @@ -88,6 +89,7 @@ export default function ComposedChart({ chartStyle, onChartStyleChange, updateDateRange, + titleTooltip, }: BarChartProps) { const { resolvedTheme } = useTheme() const [_activePayload, setActivePayload] = useState(null) @@ -151,10 +153,17 @@ export default function ComposedChart({ att.name !== maxAttribute?.attribute && attributes.some((attr) => attr.attribute === att.name && attr.enabled !== false) ) - .map((att, index) => ({ - ...att, - color: STACKED_CHART_COLORS[index % STACKED_CHART_COLORS.length], - })) + .map((att, index) => { + const attribute = attributes.find((attr) => attr.attribute === att.name) + return { + ...att, + color: attribute?.color + ? resolvedTheme?.includes('dark') + ? attribute.color.dark + : attribute.color.light + : STACKED_CHART_COLORS[index % STACKED_CHART_COLORS.length], + } + }) : [] const lastDataPoint = !!data[data.length - 1] @@ -239,6 +248,7 @@ export default function ComposedChart({ onChartStyleChange={onChartStyleChange} showMaxValue={_showMaxValue} setShowMaxValue={maxAttribute ? setShowMaxValue : undefined} + titleTooltip={titleTooltip} /> handleMouseEnter(entry.name)} + onMouseOver={() => handleMouseEnter(entry.name)} onMouseOutCapture={handleMouseLeave} > {!!attribute?.tooltip ? ( diff --git a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx index 75e06e7d838..ccbae8796b9 100644 --- a/apps/studio/components/ui/Charts/ComposedChartHandler.tsx +++ b/apps/studio/components/ui/Charts/ComposedChartHandler.tsx @@ -24,7 +24,6 @@ export type MultiAttribute = { attribute: string provider: Provider label?: string - color?: string format?: 'percent' | 'number' description?: string docsLink?: string @@ -33,6 +32,10 @@ export type MultiAttribute = { omitFromTotal?: boolean tooltip?: string enabled?: boolean + color?: { + light: string + dark: string + } } interface ComposedChartHandlerProps { @@ -57,6 +60,7 @@ interface ComposedChartHandlerProps { updateDateRange: UpdateDateRange valuePrecision?: number isVisible?: boolean + titleTooltip?: string } /** @@ -126,6 +130,7 @@ const ComposedChartHandler = ({ updateDateRange, valuePrecision, isVisible = true, + titleTooltip, }: PropsWithChildren) => { const router = useRouter() const { ref } = router.query @@ -284,6 +289,7 @@ const ComposedChartHandler = ({ updateDateRange={updateDateRange} valuePrecision={valuePrecision} hideChartType={hideChartType} + titleTooltip={titleTooltip} /> diff --git a/apps/studio/data/reports/auth-charts.ts b/apps/studio/data/reports/auth-charts.ts index 9a9869257d3..071fa819f46 100644 --- a/apps/studio/data/reports/auth-charts.ts +++ b/apps/studio/data/reports/auth-charts.ts @@ -2,7 +2,7 @@ export const getAuthReportAttributes = (isFreePlan: boolean) => [ // User Authentication Metrics { id: 'signups-by-provider', - label: 'Total Sign Ups', + label: 'Sign Ups', valuePrecision: 0, hide: false, showTooltip: true, @@ -10,6 +10,7 @@ export const getAuthReportAttributes = (isFreePlan: boolean) => [ showMaxValue: false, hideChartType: false, defaultChartStyle: 'bar', + titleTooltip: 'The total number of sign ups by provider.', attributes: [ { attribute: 'Email', provider: 'mock', label: 'Email', enabled: true }, { attribute: 'Phone', provider: 'mock', label: 'Phone', enabled: false }, @@ -36,20 +37,21 @@ export const getAuthReportAttributes = (isFreePlan: boolean) => [ ], }, // Active Users - Only DAU - { - id: 'active-users', - label: 'Active Users', - valuePrecision: 0, - hide: false, - showTooltip: false, - showLegend: false, - showMaxValue: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { attribute: 'DAU', provider: 'mock', label: 'Daily Active Users', enabled: true }, - ], - }, + // { + // id: 'active-users', + // label: 'Active Users', + // valuePrecision: 0, + // hide: false, + // showTooltip: false, + // showLegend: false, + // showMaxValue: false, + // hideChartType: false, + // defaultChartStyle: 'bar', + // titleTooltip: 'The total number of active users by provider.', + // attributes: [ + // { attribute: 'DAU', provider: 'mock', label: 'Daily Active Users', enabled: true }, + // ], + // }, // New vs Returning // { // id: 'user-types', @@ -67,22 +69,109 @@ export const getAuthReportAttributes = (isFreePlan: boolean) => [ // ], // }, // Authentication Sessions + // { + // id: 'auth-sessions', + // label: 'Auth Sessions', + // valuePrecision: 0, + // hide: false, + // showTooltip: false, + // showLegend: false, + // showMaxValue: false, + // hideChartType: false, + // defaultChartStyle: 'bar', + // titleTooltip: 'The total number of active users by provider.', + // attributes: [ + // { attribute: 'TotalSessions', provider: 'mock', label: 'Total Active Sessions' }, + // // { attribute: 'WebSessions', provider: 'mock', label: 'Web Sessions' }, + // // { attribute: 'MobileSessions', provider: 'mock', label: 'Mobile Sessions' }, + // ], + // }, { id: 'auth-sessions', - label: 'Auth Sessions', + label: 'Sessions', valuePrecision: 0, hide: false, - showTooltip: false, - showLegend: false, + showTooltip: true, + showLegend: true, showMaxValue: false, hideChartType: false, defaultChartStyle: 'bar', + titleTooltip: 'The total number of auth sessions by provider.', attributes: [ - { attribute: 'TotalSessions', provider: 'mock', label: 'Total Active Sessions' }, - // { attribute: 'WebSessions', provider: 'mock', label: 'Web Sessions' }, - // { attribute: 'MobileSessions', provider: 'mock', label: 'Mobile Sessions' }, + { attribute: 'EmailSessions', provider: 'mock', label: 'Email', enabled: true }, + { attribute: 'PhoneSessions', provider: 'mock', label: 'Phone', enabled: false }, + { attribute: 'SAMLSessions', provider: 'mock', label: 'SAML 2.0', enabled: false }, + { attribute: 'AppleSessions', provider: 'mock', label: 'Apple', enabled: false }, + { attribute: 'AzureSessions', provider: 'mock', label: 'Azure', enabled: false }, + { attribute: 'BitbucketSessions', provider: 'mock', label: 'Bitbucket', enabled: false }, + { attribute: 'DiscordSessions', provider: 'mock', label: 'Discord', enabled: false }, + { attribute: 'FacebookSessions', provider: 'mock', label: 'Facebook', enabled: false }, + { attribute: 'FigmaSessions', provider: 'mock', label: 'Figma', enabled: false }, + { attribute: 'GitHubSessions', provider: 'mock', label: 'GitHub', enabled: true }, + { attribute: 'GitLabSessions', provider: 'mock', label: 'GitLab', enabled: false }, + { attribute: 'GoogleSessions', provider: 'mock', label: 'Google', enabled: true }, + { attribute: 'KakaoSessions', provider: 'mock', label: 'Kakao', enabled: false }, + { attribute: 'KeyCloakSessions', provider: 'mock', label: 'KeyCloak', enabled: false }, + { attribute: 'LinkedInSessions', provider: 'mock', label: 'LinkedIn (OIDC)', enabled: false }, + { attribute: 'NotionSessions', provider: 'mock', label: 'Notion', enabled: false }, + { attribute: 'TwitchSessions', provider: 'mock', label: 'Twitch', enabled: false }, + { attribute: 'TwitterSessions', provider: 'mock', label: 'Twitter', enabled: false }, + { attribute: 'SlackSessions', provider: 'mock', label: 'Slack (OIDC)', enabled: false }, + { attribute: 'SpotifySessions', provider: 'mock', label: 'Spotify', enabled: false }, + { attribute: 'WorkOSSessions', provider: 'mock', label: 'WorkOS', enabled: false }, + { attribute: 'ZoomSessions', provider: 'mock', label: 'Zoom', enabled: false }, ], }, + { + id: 'churn', + label: 'Churn', + valuePrecision: 0, + hide: false, + showTooltip: true, + showLegend: true, + showMaxValue: false, + hideChartType: false, + defaultChartStyle: 'bar', + titleTooltip: 'The number of users who have deleted their account.', + attributes: [ + { attribute: 'EmailChurn', provider: 'mock', label: 'Email', enabled: true }, + { attribute: 'PhoneChurn', provider: 'mock', label: 'Phone', enabled: false }, + { attribute: 'SAMLChurn', provider: 'mock', label: 'SAML 2.0', enabled: false }, + { attribute: 'AppleChurn', provider: 'mock', label: 'Apple', enabled: false }, + { attribute: 'AzureChurn', provider: 'mock', label: 'Azure', enabled: false }, + { attribute: 'BitbucketChurn', provider: 'mock', label: 'Bitbucket', enabled: false }, + { attribute: 'DiscordChurn', provider: 'mock', label: 'Discord', enabled: false }, + { attribute: 'FacebookChurn', provider: 'mock', label: 'Facebook', enabled: false }, + { attribute: 'FigmaChurn', provider: 'mock', label: 'Figma', enabled: false }, + { attribute: 'GitHubChurn', provider: 'mock', label: 'GitHub', enabled: true }, + { attribute: 'GitLabChurn', provider: 'mock', label: 'GitLab', enabled: false }, + { attribute: 'GoogleChurn', provider: 'mock', label: 'Google', enabled: true }, + { attribute: 'KakaoChurn', provider: 'mock', label: 'Kakao', enabled: false }, + { attribute: 'KeyCloakChurn', provider: 'mock', label: 'KeyCloak', enabled: false }, + { attribute: 'LinkedInChurn', provider: 'mock', label: 'LinkedIn (OIDC)', enabled: false }, + { attribute: 'NotionChurn', provider: 'mock', label: 'Notion', enabled: false }, + { attribute: 'TwitchChurn', provider: 'mock', label: 'Twitch', enabled: false }, + { attribute: 'TwitterChurn', provider: 'mock', label: 'Twitter', enabled: false }, + { attribute: 'SlackChurn', provider: 'mock', label: 'Slack (OIDC)', enabled: false }, + { attribute: 'SpotifyChurn', provider: 'mock', label: 'Spotify', enabled: false }, + { attribute: 'WorkOSChurn', provider: 'mock', label: 'WorkOS', enabled: false }, + { attribute: 'ZoomChurn', provider: 'mock', label: 'Zoom', enabled: false }, + ], + }, + { + id: 'churn-rate', + label: 'Churn Rate', + valuePrecision: 1, + hide: false, + showTooltip: true, + showLegend: true, + showMaxValue: false, + hideChartType: false, + defaultChartStyle: 'bar', + format: '%', + titleTooltip: 'The percentage of users who have deleted their account.', + attributes: [{ attribute: 'ChurnRate', provider: 'mock', label: 'Churn Rate', enabled: true }], + }, // Session Duration // { // id: 'session-duration', @@ -111,6 +200,7 @@ export const getAuthReportAttributes = (isFreePlan: boolean) => [ showMaxValue: false, hideChartType: false, defaultChartStyle: 'bar', + titleTooltip: 'The total number of password resets.', attributes: [ { attribute: 'PasswordResetRequests', provider: 'mock', label: 'Reset Requests' }, { attribute: 'PasswordResetCompleted', provider: 'mock', label: 'Completed Resets' }, @@ -191,11 +281,44 @@ export const getAuthReportAttributes = (isFreePlan: boolean) => [ showMaxValue: false, hideChartType: false, defaultChartStyle: 'bar', + titleTooltip: 'The total number of auth errors.', attributes: [ - { attribute: 'Status403', provider: 'mock', label: '403 Forbidden' }, - { attribute: 'Status422', provider: 'mock', label: '422 Unprocessable Entity' }, - { attribute: 'Status429', provider: 'mock', label: '429 Too Many Requests' }, - { attribute: 'Status500', provider: 'mock', label: '500 Internal Server Error' }, + { + attribute: 'Status403', + provider: 'mock', + label: '403 Forbidden', + color: { + light: '#FFB74D', // Orange 300 + dark: '#FFCC80', // Orange 200 + }, + }, + { + attribute: 'Status422', + provider: 'mock', + label: '422 Unprocessable Entity', + color: { + light: '#FF9800', // Orange 500 + dark: '#FFB74D', // Orange 300 + }, + }, + { + attribute: 'Status429', + provider: 'mock', + label: '429 Too Many Requests', + color: { + light: '#E65100', // Orange 900 + dark: '#F57C00', // Orange 700 + }, + }, + { + attribute: 'Status500', + provider: 'mock', + label: '500 Internal Server Error', + color: { + light: '#B71C1C', // Red 900 + dark: '#D32F2F', // Red 700 + }, + }, ], }, // Error Rate by Provider @@ -216,27 +339,6 @@ export const getAuthReportAttributes = (isFreePlan: boolean) => [ // { attribute: 'FacebookErrorRate', provider: 'mock', label: 'Facebook' }, // ], // }, - // Rate Limiting - { - id: 'rate-limiting', - label: 'Rate Limiting Events', - valuePrecision: 0, - hide: false, - showTooltip: true, - showLegend: true, - showMaxValue: false, - hideChartType: false, - defaultChartStyle: 'bar', - attributes: [ - { attribute: 'IPRateLimited', provider: 'mock', label: 'IP Address Rate Limited' }, - { attribute: 'UserRateLimited', provider: 'mock', label: 'User Account Rate Limited' }, - { - attribute: 'BruteForceAttempts', - provider: 'mock', - label: 'Potential Brute Force Attempts', - }, - ], - }, // API Latency { id: 'auth-latency', @@ -248,12 +350,35 @@ export const getAuthReportAttributes = (isFreePlan: boolean) => [ showMaxValue: false, hideChartType: false, defaultChartStyle: 'bar', + titleTooltip: 'The average latency of auth API requests.', attributes: [ { attribute: 'SignInLatency', provider: 'mock', label: 'Sign In' }, { attribute: 'SignUpLatency', provider: 'mock', label: 'Sign Up' }, { attribute: 'TokenRefreshLatency', provider: 'mock', label: 'Token Refresh' }, ], }, + // Rate Limiting + { + id: 'rate-limiting', + label: 'Rate Limiting Events', + valuePrecision: 0, + hide: false, + showTooltip: true, + showLegend: true, + showMaxValue: false, + hideChartType: false, + defaultChartStyle: 'bar', + titleTooltip: 'The total number of rate limiting events.', + attributes: [ + { attribute: 'IPRateLimited', provider: 'mock', label: 'IP Address Rate Limited' }, + { attribute: 'UserRateLimited', provider: 'mock', label: 'User Account Rate Limited' }, + { + attribute: 'BruteForceAttempts', + provider: 'mock', + label: 'Potential Brute Force Attempts', + }, + ], + }, // Security Events // { // id: 'security-events', @@ -276,22 +401,23 @@ export const getAuthReportAttributes = (isFreePlan: boolean) => [ // ], // }, // Token Usage - // { - // id: 'token-usage', - // label: 'Token Activity', - // valuePrecision: 0, - // hide: false, - // showTooltip: true, - // showLegend: true, - // showMaxValue: false, - // hideChartType: false, - // defaultChartStyle: 'bar', - // attributes: [ - // { attribute: 'TokenIssuance', provider: 'mock', label: 'Tokens Issued' }, - // { attribute: 'TokenRefresh', provider: 'mock', label: 'Token Refreshes' }, - // { attribute: 'TokenRevocation', provider: 'mock', label: 'Token Revocations' }, - // ], - // }, + { + id: 'token-usage', + label: 'Token Activity', + valuePrecision: 0, + hide: false, + showTooltip: true, + showLegend: true, + showMaxValue: false, + hideChartType: false, + defaultChartStyle: 'bar', + titleTooltip: 'The total number of token activity.', + attributes: [ + { attribute: 'TokenIssuance', provider: 'mock', label: 'Tokens Issued' }, + { attribute: 'TokenRefresh', provider: 'mock', label: 'Token Refreshes' }, + { attribute: 'TokenRevocation', provider: 'mock', label: 'Token Revocations' }, + ], + }, // Conversion Metrics // Auth Conversion Funnel @@ -443,6 +569,109 @@ export const getAuthRequestsMockData = (provider: string) => { } } +// Add new mock data generator for provider-specific sessions +export const getProviderSessionsMockData = (provider: string) => { + let values: number[] = [] + + // Generate higher base values for sessions compared to signups + if (provider === 'EmailSessions') { + values = generatePatternedValues(DEFAULT_TIME_POINTS.length, 15, 8, 0.1, 2) + } else if (provider === 'GoogleSessions') { + values = generatePatternedValues(DEFAULT_TIME_POINTS.length, 12, 6, 0.1, 2.5) + } else if (provider === 'GitHubSessions') { + values = generatePatternedValues(DEFAULT_TIME_POINTS.length, 10, 5, 0.1, 2.2) + } else { + // For other providers, generate lower values + values = generatePatternedValues(DEFAULT_TIME_POINTS.length, 5, 3, 0.1, 2) + } + + const data = DEFAULT_TIME_POINTS.map((time, index) => ({ + period_start: time, + [provider]: values[index], + })) + + const total = values.reduce((sum, val) => sum + val, 0) + const totalAverage = total / values.length + + return { + data, + yAxisLimit: Math.max(...values) * 1.2, + format: '', + totalAverage: parseFloat(totalAverage.toFixed(2)), + total, + } +} + +// Add new mock data generator for provider-specific churn rates +export const getProviderChurnMockData = (provider: string) => { + let values: number[] = [] + + // Generate realistic churn rates (typically between 2-15%) + if (provider === 'EmailChurn') { + // Email typically has higher churn + values = generatePatternedValues(DEFAULT_TIME_POINTS.length, 2, 10, 0.1, 5.2).map((v) => + Math.min(v, 20) + ) + } else if (provider === 'GoogleChurn') { + // Google typically has lower churn due to account persistence + values = generatePatternedValues(DEFAULT_TIME_POINTS.length, 8, 2, 0.1, 1.3).map((v) => + Math.min(v, 15) + ) + } else if (provider === 'GitHubChurn') { + // GitHub has moderate churn + values = generatePatternedValues(DEFAULT_TIME_POINTS.length, 10, 2.5, 0.1, 1.2).map((v) => + Math.min(v, 18) + ) + } else { + // Other providers have varying churn rates + values = generatePatternedValues(DEFAULT_TIME_POINTS.length, 9, 3, 0.1, 1.4).map((v) => + Math.min(v, 20) + ) + } + + const data = DEFAULT_TIME_POINTS.map((time, index) => ({ + period_start: time, + [provider]: values[index], + })) + + const total = values.reduce((sum, val) => sum + val, 0) + const totalAverage = total / values.length + + return { + data, + yAxisLimit: 20, // Max 20% churn rate + format: '%', + totalAverage: parseFloat(totalAverage.toFixed(1)), + total, + } +} + +// Add new mock data generator for churn rates +export const getChurnRateMockData = (attribute: string) => { + let values: number[] = [] + + // Overall churn rate with some variance + values = generatePatternedValues(DEFAULT_TIME_POINTS.length, 0.6, 2, 0.15, 2.6) + .map((v) => Math.max(0, v - 0.25)) // Allow some values to go to 0 + .map((v) => Math.min(v, 3.2)) + + const data = DEFAULT_TIME_POINTS.map((time, index) => ({ + period_start: time, + [attribute]: values[index], + })) + + const total = values.reduce((sum, val) => sum + val, 0) + const totalAverage = total / values.length + + return { + data, + yAxisLimit: 1.5, // Max 1.5% churn rate + format: '%', + totalAverage: parseFloat(totalAverage.toFixed(1)), + total, + } +} + // Mock data generators for new charts export const getActiveUsersMockData = (metric: string) => { const length = DEFAULT_TIME_POINTS.length @@ -744,11 +973,44 @@ export const getErrorSecurityChartAttributes = () => [ showMaxValue: false, hideChartType: false, defaultChartStyle: 'bar', + titleTooltip: 'The total number of auth errors.', attributes: [ - { attribute: 'Status403', provider: 'mock', label: '403 Forbidden' }, - { attribute: 'Status422', provider: 'mock', label: '422 Unprocessable Entity' }, - { attribute: 'Status429', provider: 'mock', label: '429 Too Many Requests' }, - { attribute: 'Status500', provider: 'mock', label: '500 Internal Server Error' }, + { + attribute: 'Status403', + provider: 'mock', + label: '403 Forbidden', + color: { + light: '#FFB74D', // Orange 300 + dark: '#FFCC80', // Orange 200 + }, + }, + { + attribute: 'Status422', + provider: 'mock', + label: '422 Unprocessable Entity', + color: { + light: '#FF9800', // Orange 500 + dark: '#FFB74D', // Orange 300 + }, + }, + { + attribute: 'Status429', + provider: 'mock', + label: '429 Too Many Requests', + color: { + light: '#E65100', // Orange 900 + dark: '#F57C00', // Orange 700 + }, + }, + { + attribute: 'Status500', + provider: 'mock', + label: '500 Internal Server Error', + color: { + light: '#B71C1C', // Red 900 + dark: '#D32F2F', // Red 700 + }, + }, ], }, // Error Rate by Provider @@ -1271,6 +1533,71 @@ export const getMockDataForAttribute = (attribute: string) => { return getAuthRequestsMockData(attribute) } + // Provider-specific sessions + if ( + [ + 'EmailSessions', + 'PhoneSessions', + 'SAMLSessions', + 'AppleSessions', + 'AzureSessions', + 'BitbucketSessions', + 'DiscordSessions', + 'FacebookSessions', + 'FigmaSessions', + 'GitHubSessions', + 'GitLabSessions', + 'GoogleSessions', + 'KakaoSessions', + 'KeyCloakSessions', + 'LinkedInSessions', + 'NotionSessions', + 'TwitchSessions', + 'TwitterSessions', + 'SlackSessions', + 'SpotifySessions', + 'WorkOSSessions', + 'ZoomSessions', + ].includes(attribute) + ) { + return getProviderSessionsMockData(attribute) + } + + // Provider-specific churn + if ( + [ + 'EmailChurn', + 'PhoneChurn', + 'SAMLChurn', + 'AppleChurn', + 'AzureChurn', + 'BitbucketChurn', + 'DiscordChurn', + 'FacebookChurn', + 'FigmaChurn', + 'GitHubChurn', + 'GitLabChurn', + 'GoogleChurn', + 'KakaoChurn', + 'KeyCloakChurn', + 'LinkedInChurn', + 'NotionChurn', + 'TwitchChurn', + 'TwitterChurn', + 'SlackChurn', + 'SpotifyChurn', + 'WorkOSChurn', + 'ZoomChurn', + ].includes(attribute) + ) { + return getProviderChurnMockData(attribute) + } + + // Overall churn rate + if (attribute === 'ChurnRate') { + return getChurnRateMockData(attribute) + } + // Active Users charts if (['DAU', 'WAU', 'MAU'].includes(attribute)) { return getActiveUsersMockData(attribute)