feat: add x ads tracking entry points (#13986)

*  feat: add x ads tracking entry points

* 🔨 chore: bump analytics to v1.6.2

* 🐛 fix: add auth analytics provider entry
This commit is contained in:
Tsuki 2026-04-20 14:12:14 +08:00 committed by GitHub
parent ed64e2b8af
commit 5dd7cd7408
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 298 additions and 91 deletions

View file

@ -266,7 +266,7 @@
"@lobechat/ssrf-safe-fetch": "workspace:*",
"@lobechat/utils": "workspace:*",
"@lobechat/web-crawler": "workspace:*",
"@lobehub/analytics": "^1.6.0",
"@lobehub/analytics": "^1.6.2",
"@lobehub/charts": "^5.0.0",
"@lobehub/desktop-ipc-typings": "workspace:*",
"@lobehub/editor": "^4.8.1",

View file

@ -1,6 +1,7 @@
import { type ReactNode } from 'react';
import { appEnv } from '@/envs/app';
import AnalyticsRSCProvider from '@/layout/AnalyticsRSCProvider';
import AuthProvider from '@/layout/AuthProvider';
import NextThemeProvider from '@/layout/GlobalProvider/NextThemeProvider';
import StyleRegistry from '@/layout/GlobalProvider/StyleRegistry';
@ -30,7 +31,9 @@ const AuthGlobalProvider = async ({ children, variants }: AuthGlobalProviderProp
segmentVariants={variants}
serverConfig={serverConfig}
>
<AuthProvider>{children}</AuthProvider>
<AnalyticsRSCProvider>
<AuthProvider>{children}</AuthProvider>
</AnalyticsRSCProvider>
</AuthServerConfigProvider>
</AuthThemeLite>
</NextThemeProvider>

View file

@ -8,6 +8,7 @@ import { type CheckUserResponseData } from '@/app/(backend)/api/auth/check-user/
import { type ResolveUsernameResponseData } from '@/app/(backend)/api/auth/resolve-username/route';
import { useBusinessSignin } from '@/business/client/hooks/useBusinessSignin';
import { message } from '@/components/AntdStaticMethods';
import { trackLoginOrSignupClicked } from '@/features/User/UserLoginOrSignup/trackLoginOrSignupClicked';
import { requestPasswordReset, signIn } from '@/libs/better-auth/auth-client';
import { isBuiltinProvider, normalizeProviderId } from '@/libs/better-auth/utils/client';
@ -125,6 +126,8 @@ export const useSignIn = () => {
const handleCheckUser = async (values: Pick<SignInFormValues, 'email'>) => {
setLoading(true);
await trackLoginOrSignupClicked({ spm: 'signin.email_step.submit' });
try {
const resolvedEmail = await resolveEmailFromIdentifier(values.email);
if (!resolvedEmail) return;
@ -172,6 +175,8 @@ export const useSignIn = () => {
const handleSignIn = async (values: Pick<SignInFormValues, 'password'>) => {
setLoading(true);
await trackLoginOrSignupClicked({ spm: 'signin.password_step.submit' });
try {
const callbackUrl = searchParams.get('callbackUrl') || '/';
const result = await signIn.email(
@ -203,6 +208,11 @@ export const useSignIn = () => {
const handleSocialSignIn = async (provider: string) => {
setSocialLoading(provider);
const normalizedProvider = normalizeProviderId(provider);
await trackLoginOrSignupClicked({
provider: normalizedProvider,
spm: 'signin.social.click',
});
try {
if (ENABLE_BUSINESS_FEATURES && !(await preSocialSigninCheck())) {
setSocialLoading(null);
@ -252,7 +262,9 @@ export const useSignIn = () => {
const params = new URLSearchParams();
if (currentEmail) params.set('email', currentEmail);
params.set('callbackUrl', callbackUrl);
router.push(`/signup?${params.toString()}`);
void trackLoginOrSignupClicked({ spm: 'signin.go_to_signup.click' }).finally(() => {
router.push(`/signup?${params.toString()}`);
});
};
const handleForgotPassword = async () => {

View file

@ -9,6 +9,7 @@ import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { AuthCard } from '../../../../../features/AuthCard';
import { trackLoginOrSignupClicked } from '../../../../../features/User/UserLoginOrSignup/trackLoginOrSignupClicked';
import { type SignUpFormValues } from './useSignUp';
import { useSignUp } from './useSignUp';
@ -27,7 +28,17 @@ const BetterAuthSignUpForm = () => {
const footer = (
<Text>
{t('betterAuth.signup.hasAccount')}{' '}
<Link href={`/signin?${searchParams.toString()}`}>{t('betterAuth.signup.signinLink')}</Link>
<Link
href={`/signin?${searchParams.toString()}`}
onClick={(event) => {
event.preventDefault();
void trackLoginOrSignupClicked({ spm: 'signup.go_to_signin.click' }).finally(() => {
window.location.href = `/signin?${searchParams.toString()}`;
});
}}
>
{t('betterAuth.signup.signinLink')}
</Link>
</Text>
);

View file

@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
import { type BusinessSignupFomData } from '@/business/client/hooks/useBusinessSignup';
import { useBusinessSignup } from '@/business/client/hooks/useBusinessSignup';
import { message } from '@/components/AntdStaticMethods';
import { trackLoginOrSignupClicked } from '@/features/User/UserLoginOrSignup/trackLoginOrSignupClicked';
import { signUp } from '@/libs/better-auth/auth-client';
import { useAuthServerConfigStore } from '../../_layout/AuthServerConfigProvider';
@ -26,6 +27,8 @@ export const useSignUp = () => {
const handleSignUp = async (values: SignUpFormValues) => {
setLoading(true);
await trackLoginOrSignupClicked({ spm: 'signup.submit.click' });
try {
if (ENABLE_BUSINESS_FEATURES && !(await preSocialSignupCheck(values))) {
setLoading(false);

View file

@ -131,6 +131,17 @@ function buildAnalyticsConfig(): AnalyticsConfig {
};
}
if (analyticsEnv.ENABLED_X_ADS && analyticsEnv.X_ADS_PIXEL_ID) {
config.xAds = {
eventIds: {
login_or_signup_clicked: analyticsEnv.X_ADS_LOGIN_OR_SIGNUP_CLICKED_EVENT_ID,
main_page_view: analyticsEnv.X_ADS_MAIN_PAGE_VIEW_EVENT_ID,
},
pixelId: analyticsEnv.X_ADS_PIXEL_ID,
purchaseEventId: analyticsEnv.X_ADS_PURCHASE_EVENT_ID,
};
}
if (analyticsEnv.REACT_SCAN_MONITOR_API_KEY) {
config.reactScan = { apiKey: analyticsEnv.REACT_SCAN_MONITOR_API_KEY };
}

View file

@ -0,0 +1,31 @@
'use client';
import { useAnalytics } from '@lobehub/analytics/react';
import { memo, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
const HomePageTracker = memo(() => {
const { analytics } = useAnalytics();
const { pathname } = useLocation();
useEffect(() => {
if (!analytics || pathname !== '/') return;
const timer = setTimeout(() => {
analytics.track({
name: 'main_page_view',
properties: {
spm: 'homepage.main_page.view',
},
});
}, 1000);
return () => clearTimeout(timer);
}, [analytics, pathname]);
return null;
});
HomePageTracker.displayName = 'HomePageTracker';
export default HomePageTracker;

View file

@ -3,11 +3,12 @@
import {
type GoogleAnalyticsProviderConfig,
type PostHogProviderAnalyticsConfig,
type XAdsProviderAnalyticsConfig,
} from '@lobehub/analytics';
import { createSingletonAnalytics } from '@lobehub/analytics';
import { AnalyticsProvider } from '@lobehub/analytics/react';
import { type ReactNode } from 'react';
import { memo, useMemo } from 'react';
import { memo, useRef } from 'react';
import { BUSINESS_LINE } from '@/const/analytics';
import { isDesktop } from '@/const/version';
@ -17,28 +18,32 @@ type Props = {
children: ReactNode;
ga4Config: GoogleAnalyticsProviderConfig;
postHogConfig: PostHogProviderAnalyticsConfig;
xAdsConfig: XAdsProviderAnalyticsConfig;
};
let analyticsInstance: ReturnType<typeof createSingletonAnalytics> | null = null;
export const LobeAnalyticsProvider = memo(
({ children, ga4Config, postHogConfig }: Props) => {
const analytics = useMemo(() => {
if (analyticsInstance) {
return analyticsInstance;
}
({ children, ga4Config, postHogConfig, xAdsConfig }: Props) => {
const analyticsRef = useRef<ReturnType<typeof createSingletonAnalytics> | null>(null);
analyticsInstance = createSingletonAnalytics({
business: BUSINESS_LINE,
debug: isDev,
providers: {
ga4: ga4Config,
posthog: postHogConfig,
},
});
if (!analyticsRef.current) {
analyticsRef.current =
analyticsInstance ||
createSingletonAnalytics({
business: BUSINESS_LINE,
debug: isDev,
providers: {
ga4: ga4Config,
posthog: postHogConfig,
xAds: xAdsConfig,
},
});
return analyticsInstance;
}, []);
analyticsInstance = analyticsRef.current;
}
const analytics = analyticsRef.current;
if (!analytics) return children;

View file

@ -30,6 +30,13 @@ export const LobeAnalyticsProviderWrapper = memo<Props>(({ children }) => {
key: analytics?.posthog?.key ?? '',
person_profiles: 'always',
}}
xAdsConfig={{
debug: isDev,
eventIds: analytics?.xAds?.eventIds,
enabled: !!analytics?.xAds?.pixelId,
pixelId: analytics?.xAds?.pixelId ?? '',
purchaseEventId: analytics?.xAds?.purchaseEventId,
}}
>
{children}
</LobeAnalyticsProvider>

View file

@ -1,52 +0,0 @@
'use client';
import { useAnalytics } from '@lobehub/analytics/react';
import { memo, useCallback, useEffect } from 'react';
import { getChatStoreState } from '@/store/chat';
import { displayMessageSelectors } from '@/store/chat/selectors';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors';
import { getSessionStoreState } from '@/store/session';
import { sessionSelectors } from '@/store/session/selectors';
const MainInterfaceTracker = memo(() => {
const { analytics } = useAnalytics();
const getMainInterfaceAnalyticsData = useCallback(() => {
const currentSession = sessionSelectors.currentSession(getSessionStoreState());
const activeSessionId = currentSession?.id;
const defaultSessions = sessionSelectors.defaultSessions(getSessionStoreState());
const showRightPanel = systemStatusSelectors.showRightPanel(useGlobalStore.getState());
const messages = displayMessageSelectors.activeDisplayMessages(getChatStoreState());
return {
active_assistant: activeSessionId === 'inbox' ? null : currentSession?.meta?.title || null,
has_chat_history: messages.length > 0,
session_id: activeSessionId ? activeSessionId : 'inbox',
sidebar_state: showRightPanel ? 'expanded' : 'collapsed',
visible_assistants_count: defaultSessions.length,
};
}, []);
useEffect(() => {
if (!analytics) return;
const timer = setTimeout(() => {
analytics.track({
name: 'main_page_view',
properties: {
...getMainInterfaceAnalyticsData(),
spm: 'main_page.interface.view',
},
});
}, 1000);
return () => clearTimeout(timer);
}, [analytics, getMainInterfaceAnalyticsData]);
return null;
});
MainInterfaceTracker.displayName = 'MainInterfaceTracker';
export default MainInterfaceTracker;

View file

@ -0,0 +1,48 @@
'use client';
import { createAnalytics, getSingletonAnalyticsOptional } from '@lobehub/analytics';
import { memo, useEffect, useRef } from 'react';
import { BUSINESS_LINE } from '@/const/analytics';
import { isDev } from '@/utils/env';
interface XAdsProps {
eventIds?: Record<string, string | undefined>;
pixelId?: string;
purchaseEventId?: string;
}
const XAds = memo<XAdsProps>(({ eventIds, pixelId, purchaseEventId }) => {
const analyticsRef = useRef<ReturnType<typeof createAnalytics> | null>(null);
useEffect(() => {
const singletonAnalytics = getSingletonAnalyticsOptional();
if (singletonAnalytics?.getProvider('xAds')) {
return;
}
if (!analyticsRef.current) {
analyticsRef.current = createAnalytics({
business: BUSINESS_LINE,
debug: isDev,
providers: {
xAds: {
debug: isDev,
eventIds,
enabled: !!pixelId,
pixelId: pixelId ?? '',
purchaseEventId,
},
},
});
}
analyticsRef.current.initialize().catch((error) => {
console.error('[X Ads Bootstrap] Initialization failed:', error);
});
}, [eventIds, pixelId, purchaseEventId]);
return null;
});
export default XAds;

View file

@ -5,6 +5,7 @@ import dynamic from '@/libs/next/dynamic';
import Desktop from './Desktop';
import Google from './Google';
import Vercel from './Vercel';
import X from './X';
const Plausible = dynamic(() => import('./Plausible'));
const Umami = dynamic(() => import('./Umami'));
@ -20,6 +21,16 @@ const Analytics = () => {
{analyticsEnv.ENABLE_GOOGLE_ANALYTICS && (
<Google gaId={analyticsEnv.GOOGLE_ANALYTICS_MEASUREMENT_ID} />
)}
{analyticsEnv.ENABLED_X_ADS && (
<X
eventIds={{
login_or_signup_clicked: analyticsEnv.X_ADS_LOGIN_OR_SIGNUP_CLICKED_EVENT_ID,
main_page_view: analyticsEnv.X_ADS_MAIN_PAGE_VIEW_EVENT_ID,
}}
pixelId={analyticsEnv.X_ADS_PIXEL_ID}
purchaseEventId={analyticsEnv.X_ADS_PURCHASE_EVENT_ID}
/>
)}
{analyticsEnv.ENABLED_PLAUSIBLE_ANALYTICS && (
<Plausible
domain={analyticsEnv.PLAUSIBLE_DOMAIN}

View file

@ -22,6 +22,10 @@ describe('getAnalyticsConfig', () => {
process.env.CLARITY_PROJECT_ID = 'clarity_id';
process.env.ENABLE_VERCEL_ANALYTICS = '1';
process.env.GOOGLE_ANALYTICS_MEASUREMENT_ID = 'ga_id';
process.env.X_ADS_PIXEL_ID = 'tw-pixel_id';
process.env.X_ADS_LOGIN_OR_SIGNUP_CLICKED_EVENT_ID = 'tw-pixel_id-login_or_signup_clicked';
process.env.X_ADS_MAIN_PAGE_VIEW_EVENT_ID = 'tw-pixel_id-main_page_view';
process.env.X_ADS_PURCHASE_EVENT_ID = 'tw-pixel_id-purchase_event_id';
const config = getAnalyticsConfig();
@ -42,6 +46,11 @@ describe('getAnalyticsConfig', () => {
DEBUG_VERCEL_ANALYTICS: false,
ENABLE_GOOGLE_ANALYTICS: true,
GOOGLE_ANALYTICS_MEASUREMENT_ID: 'ga_id',
ENABLED_X_ADS: true,
X_ADS_PIXEL_ID: 'tw-pixel_id',
X_ADS_LOGIN_OR_SIGNUP_CLICKED_EVENT_ID: 'tw-pixel_id-login_or_signup_clicked',
X_ADS_MAIN_PAGE_VIEW_EVENT_ID: 'tw-pixel_id-main_page_view',
X_ADS_PURCHASE_EVENT_ID: 'tw-pixel_id-purchase_event_id',
});
});
});

View file

@ -26,6 +26,12 @@ export const getAnalyticsConfig = () => {
ENABLE_GOOGLE_ANALYTICS: z.boolean(),
GOOGLE_ANALYTICS_MEASUREMENT_ID: z.string().optional(),
ENABLED_X_ADS: z.boolean(),
X_ADS_PIXEL_ID: z.string().optional(),
X_ADS_LOGIN_OR_SIGNUP_CLICKED_EVENT_ID: z.string().optional(),
X_ADS_MAIN_PAGE_VIEW_EVENT_ID: z.string().optional(),
X_ADS_PURCHASE_EVENT_ID: z.string().optional(),
REACT_SCAN_MONITOR_API_KEY: z.string().optional(),
},
runtimeEnv: {
@ -57,6 +63,13 @@ export const getAnalyticsConfig = () => {
ENABLE_GOOGLE_ANALYTICS: !!process.env.GOOGLE_ANALYTICS_MEASUREMENT_ID,
GOOGLE_ANALYTICS_MEASUREMENT_ID: process.env.GOOGLE_ANALYTICS_MEASUREMENT_ID,
// X Ads
ENABLED_X_ADS: !!process.env.X_ADS_PIXEL_ID,
X_ADS_PIXEL_ID: process.env.X_ADS_PIXEL_ID,
X_ADS_LOGIN_OR_SIGNUP_CLICKED_EVENT_ID: process.env.X_ADS_LOGIN_OR_SIGNUP_CLICKED_EVENT_ID,
X_ADS_MAIN_PAGE_VIEW_EVENT_ID: process.env.X_ADS_MAIN_PAGE_VIEW_EVENT_ID,
X_ADS_PURCHASE_EVENT_ID: process.env.X_ADS_PURCHASE_EVENT_ID,
// React Scan Monitor
// https://dashboard.react-scan.com
REACT_SCAN_MONITOR_API_KEY: process.env.REACT_SCAN_MONITOR_API_KEY,

View file

@ -1,22 +1,15 @@
import { useAnalytics } from '@lobehub/analytics/react';
import { Button, Flexbox } from '@lobehub/ui';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import UserInfo from '../UserInfo';
import { trackLoginOrSignupClicked } from './trackLoginOrSignupClicked';
const UserLoginOrSignup = memo<{ onClick: () => void }>(({ onClick }) => {
const { t } = useTranslation('auth');
const { analytics } = useAnalytics();
const handleClick = () => {
analytics?.track({
name: 'login_or_signup_clicked',
properties: {
spm: 'homepage.login_or_signup.click',
},
});
void trackLoginOrSignupClicked({ spm: 'homepage.login_or_signup.click' });
onClick();
};

View file

@ -0,0 +1,34 @@
import { getSingletonAnalyticsOptional } from '@lobehub/analytics';
interface TrackLoginOrSignupClickedParams {
provider?: string;
spm: string;
}
export const trackLoginOrSignupClicked = ({
provider,
spm,
}: TrackLoginOrSignupClickedParams) => {
const analytics = getSingletonAnalyticsOptional();
if (!analytics) return Promise.resolve();
const sendEvent = async () => {
const status = analytics.getStatus();
if (!status.initialized) {
await analytics.initialize();
}
await analytics.track({
name: 'login_or_signup_clicked',
properties: {
...(provider && { provider }),
spm,
},
});
};
return sendEvent().catch((error) => {
console.error('Failed to track login_or_signup_clicked:', error);
});
};

View file

@ -0,0 +1,45 @@
import type { ReactNode } from 'react';
import { LobeAnalyticsProvider } from '@/components/Analytics/LobeAnalyticsProvider';
import { analyticsEnv } from '@/envs/analytics';
import { isDev } from '@/utils/env';
interface AnalyticsRSCProviderProps {
children: ReactNode;
}
const AnalyticsRSCProvider = ({ children }: AnalyticsRSCProviderProps) => {
return (
<LobeAnalyticsProvider
ga4Config={{
debug: isDev,
enabled: !!analyticsEnv.GOOGLE_ANALYTICS_MEASUREMENT_ID,
gtagConfig: {
debug_mode: isDev,
},
measurementId: analyticsEnv.GOOGLE_ANALYTICS_MEASUREMENT_ID ?? '',
}}
postHogConfig={{
debug: analyticsEnv.DEBUG_POSTHOG_ANALYTICS,
enabled: analyticsEnv.ENABLED_POSTHOG_ANALYTICS,
host: analyticsEnv.POSTHOG_HOST,
key: analyticsEnv.POSTHOG_KEY ?? '',
person_profiles: 'always',
}}
xAdsConfig={{
debug: isDev,
eventIds: {
login_or_signup_clicked: analyticsEnv.X_ADS_LOGIN_OR_SIGNUP_CLICKED_EVENT_ID,
main_page_view: analyticsEnv.X_ADS_MAIN_PAGE_VIEW_EVENT_ID,
},
enabled: analyticsEnv.ENABLED_X_ADS,
pixelId: analyticsEnv.X_ADS_PIXEL_ID ?? '',
purchaseEventId: analyticsEnv.X_ADS_PURCHASE_EVENT_ID,
}}
>
{children}
</LobeAnalyticsProvider>
);
};
export default AnalyticsRSCProvider;

View file

@ -3,8 +3,6 @@
import { Flexbox } from '@lobehub/ui';
import { memo } from 'react';
import MainInterfaceTracker from '@/components/Analytics/MainInterfaceTracker';
import Conversation from './features/Conversation';
import AgentWorkingSidebar from './features/Conversation/WorkingSidebar';
import PageTitle from './features/PageTitle';
@ -25,7 +23,6 @@ const ChatPage = memo(() => {
<Portal />
<AgentWorkingSidebar />
</Flexbox>
<MainInterfaceTracker />
<TelemetryNotification mobile={false} />
</>
);

View file

@ -3,8 +3,6 @@
import { Flexbox } from '@lobehub/ui';
import { memo } from 'react';
import MainInterfaceTracker from '@/components/Analytics/MainInterfaceTracker';
import Conversation from './features/Conversation';
import PageTitle from './features/PageTitle';
import Portal from './features/Portal';
@ -23,7 +21,6 @@ const ChatPage = memo(() => {
<Conversation />
<Portal />
</Flexbox>
<MainInterfaceTracker />
<TelemetryNotification mobile={false} />
</>
);

View file

@ -2,6 +2,7 @@ import { Flexbox } from '@lobehub/ui';
import { type FC } from 'react';
import { useLocation } from 'react-router-dom';
import HomePageTracker from '@/components/Analytics/HomePageTracker';
import PageTitle from '@/components/PageTitle';
import NavHeader from '@/features/NavHeader';
import WideScreenContainer from '@/features/WideScreenContainer';
@ -15,6 +16,7 @@ const Home: FC = () => {
return (
<>
{isHomeRoute && <PageTitle title="" />}
<HomePageTracker />
<NavHeader right={<Flexbox horizontal align="center" />} />
<Flexbox height={'100%'} style={{ overflowY: 'auto', paddingBottom: '16vh' }} width={'100%'}>
<WideScreenContainer>

View file

@ -2,7 +2,6 @@
import { memo } from 'react';
import MainInterfaceTracker from '@/components/Analytics/MainInterfaceTracker';
import ConversationArea from '@/routes/(main)/agent/features/Conversation/ConversationArea';
import PageTitle from '@/routes/(main)/agent/features/PageTitle';
import PortalPanel from '@/routes/(main)/agent/features/Portal/features/PortalPanel';
@ -17,7 +16,6 @@ const MobileChatPage = memo(() => {
<ConversationArea />
<Topic />
<PortalPanel mobile />
<MainInterfaceTracker />
<TelemetryNotification mobile />
</>
);

View file

@ -10,6 +10,7 @@ import { Link, Outlet } from 'react-router-dom';
import { ProductLogo } from '@/components/Branding';
import Loading from '@/components/Loading/BrandTextLoading';
import { trackLoginOrSignupClicked } from '@/features/User/UserLoginOrSignup/trackLoginOrSignupClicked';
import { useIsDark } from '@/hooks/useIsDark';
import { useUserStore } from '@/store/user';
import { authSelectors } from '@/store/user/slices/auth/selectors';
@ -44,7 +45,18 @@ const ShareTopicLayout = memo<PropsWithChildren>(({ children }) => {
<ProductLogo size={32} />
</Link>
) : (
<NextLink href="/signin" style={{ color: 'inherit' }}>
<NextLink
href="/signin"
style={{ color: 'inherit' }}
onClick={(event) => {
event.preventDefault();
void trackLoginOrSignupClicked({ spm: 'share.logo_to_signin.click' }).finally(
() => {
window.location.href = '/signin';
},
);
}}
>
<ProductLogo size={32} />
</NextLink>
)}

View file

@ -10,6 +10,7 @@ import useSWR from 'swr';
import NotFound from '@/components/404';
import Loading from '@/components/Loading/BrandTextLoading';
import { trackLoginOrSignupClicked } from '@/features/User/UserLoginOrSignup/trackLoginOrSignupClicked';
import { lambdaClient } from '@/libs/trpc/client';
import ActionBar from './features/ActionBar';
@ -59,7 +60,18 @@ const ShareTopicPage = memo(() => {
status={''}
title={t('sharePage.error.unauthorized.title')}
extra={
<Button href="/signin" type="primary">
<Button
href="/signin"
type="primary"
onClick={(event) => {
event.preventDefault();
void trackLoginOrSignupClicked({
spm: 'share.unauthorized.signin.click',
}).finally(() => {
window.location.href = '/signin';
});
}}
>
{t('sharePage.error.unauthorized.action')}
</Button>
}

View file

@ -10,6 +10,11 @@ export interface AnalyticsConfig {
reactScan?: { apiKey: string };
umami?: { scriptUrl: string; websiteId: string };
vercel?: { debug: boolean; enabled: boolean };
xAds?: {
eventIds?: Record<string, string | undefined>;
pixelId: string;
purchaseEventId?: string;
};
}
export interface SPAClientEnv {