[web] workspace/sandpack: also switch bundler URL when timeout

This commit is contained in:
Yangshun 2025-07-22 14:17:17 +08:00
parent 13ab5faf1a
commit 29e3419214
8 changed files with 82 additions and 20 deletions

View File

@ -8,6 +8,7 @@ import CodingPreferencesProvider from '~/components/global/CodingPreferencesProv
import { useColorSchemePreferences } from '~/components/global/color-scheme/ColorSchemePreferencesProvider'; import { useColorSchemePreferences } from '~/components/global/color-scheme/ColorSchemePreferencesProvider';
import type { ProjectsChallengeSolutionBundle } from '~/components/projects/challenges/types'; import type { ProjectsChallengeSolutionBundle } from '~/components/projects/challenges/types';
import SandpackObservability from '~/components/workspace/common/sandpack/SandpackObservability'; import SandpackObservability from '~/components/workspace/common/sandpack/SandpackObservability';
import { SandpackTimeout } from '~/components/workspace/common/sandpack/SandpackTimeout';
import { useSandpackBundlerURL } from '~/components/workspace/common/sandpack/useSandpackBundlerURL'; import { useSandpackBundlerURL } from '~/components/workspace/common/sandpack/useSandpackBundlerURL';
import ProjectsChallengeSolutionWorkspace from './ProjectsChallengeSolutionWorkspace'; import ProjectsChallengeSolutionWorkspace from './ProjectsChallengeSolutionWorkspace';
@ -20,7 +21,8 @@ const sandpackO11yInstance = 'projects.challenge_solution';
export default function ProjectsChallengeSolutionSection({ solution }: Props) { export default function ProjectsChallengeSolutionSection({ solution }: Props) {
const { colorScheme } = useColorSchemePreferences(); const { colorScheme } = useColorSchemePreferences();
const bundlerURL = useSandpackBundlerURL(sandpackO11yInstance); const [bundlerURL, changeToFallbackUrl] =
useSandpackBundlerURL(sandpackO11yInstance);
const { files, workspace } = solution; const { files, workspace } = solution;
return ( return (
@ -49,6 +51,10 @@ export default function ProjectsChallengeSolutionSection({ solution }: Props) {
activeTabScrollIntoView={true} activeTabScrollIntoView={true}
defaultFiles={files} defaultFiles={files}
/> />
<SandpackTimeout
instance={sandpackO11yInstance}
onTimeout={changeToFallbackUrl}
/>
<SandpackObservability <SandpackObservability
bundlerURL={bundlerURL} bundlerURL={bundlerURL}
instance={sandpackO11yInstance} instance={sandpackO11yInstance}

View File

@ -73,20 +73,8 @@ export default function SandpackObservability({ bundlerURL, instance }: Props) {
const { status: sandpackStatus } = sandpack; const { status: sandpackStatus } = sandpack;
const loadingStartedRef = useRef(false); const loadingStartedRef = useRef(false);
const readySentRef = useRef(false); const readySentRef = useRef(false);
const timeoutSentRef = useRef(false);
usePingSandpackBundler({ bundlerURL, instance }); usePingSandpackBundler({ bundlerURL, instance });
useEffect(() => {
if (sandpackStatus === 'timeout' && !timeoutSentRef.current) {
logEvent('sandpack.timeout', {
instance,
namespace: 'workspace',
});
timeoutSentRef.current = true;
}
}, [instance, sandpackStatus]);
useEffect(() => { useEffect(() => {
if (loadingStartedRef.current) { if (loadingStartedRef.current) {
return; return;

View File

@ -0,0 +1,29 @@
import { useSandpack } from '@codesandbox/sandpack-react';
import { useEffect, useRef } from 'react';
import logEvent from '~/logging/logEvent';
type Props = Readonly<{
instance: string;
onTimeout: (instance: string) => void;
}>;
export function SandpackTimeout({ instance, onTimeout }: Props) {
const timeoutSentRef = useRef(false);
const { sandpack } = useSandpack();
const { status: sandpackStatus } = sandpack;
useEffect(() => {
if (sandpackStatus === 'timeout' && !timeoutSentRef.current) {
onTimeout(instance);
logEvent('sandpack.timeout', {
instance,
namespace: 'workspace',
});
timeoutSentRef.current = true;
}
}, [instance, sandpackStatus, onTimeout]);
return null;
}

View File

@ -8,7 +8,7 @@ import { getErrorMessage } from '~/utils/getErrorMessage';
const defaultBundlerURL = 'https://bundler.greatfrontend.io'; const defaultBundlerURL = 'https://bundler.greatfrontend.io';
const fallbackBundlerURL = 'https://bundler.greatfrontend.com'; const fallbackBundlerURL = 'https://bundler.greatfrontend.com';
export function useSandpackBundlerURL(instance: string): string { export function useSandpackBundlerURL(instance: string) {
const [url, setUrl] = useGreatStorageLocal( const [url, setUrl] = useGreatStorageLocal(
'workspace:bundler-url', // Change the key if you want to reset the URL in local storage 'workspace:bundler-url', // Change the key if you want to reset the URL in local storage
defaultBundlerURL, defaultBundlerURL,
@ -17,6 +17,20 @@ export function useSandpackBundlerURL(instance: string): string {
}, },
); );
const changeToFallbackUrl = useCallback(
(reason: string) => {
setUrl(fallbackBundlerURL);
logEvent('sandpack.bundler_fallback', {
instance,
namespace: 'workspace',
online: navigator.onLine,
reason,
url: fallbackBundlerURL,
});
},
[instance, setUrl],
);
const pingBundlerURL = useCallback(async () => { const pingBundlerURL = useCallback(async () => {
try { try {
const response = await fetch(new URL('version.txt', url).toString()); const response = await fetch(new URL('version.txt', url).toString());
@ -33,6 +47,7 @@ export function useSandpackBundlerURL(instance: string): string {
instance, instance,
namespace: 'workspace', namespace: 'workspace',
online: navigator.onLine, online: navigator.onLine,
reason: 'blocked',
stack: error instanceof Error ? error.stack : null, stack: error instanceof Error ? error.stack : null,
url, url,
}); });
@ -68,5 +83,5 @@ export function useSandpackBundlerURL(instance: string): string {
pingBundlerURL(); pingBundlerURL();
}, [url, pingBundlerURL]); }, [url, pingBundlerURL]);
return url; return [url, changeToFallbackUrl] as const;
} }

View File

@ -8,6 +8,7 @@ import type {
QuestionJavaScript, QuestionJavaScript,
QuestionMetadata, QuestionMetadata,
} from '~/components/interviews/questions/common/QuestionsTypes'; } from '~/components/interviews/questions/common/QuestionsTypes';
import { SandpackTimeout } from '~/components/workspace/common/sandpack/SandpackTimeout';
import JavaScriptCodingWorkspace from '~/components/workspace/javascript/JavaScriptCodingWorkspace'; import JavaScriptCodingWorkspace from '~/components/workspace/javascript/JavaScriptCodingWorkspace';
import { loadLocalJavaScriptQuestionCode } from '~/components/workspace/javascript/JavaScriptCodingWorkspaceCodeStorage'; import { loadLocalJavaScriptQuestionCode } from '~/components/workspace/javascript/JavaScriptCodingWorkspaceCodeStorage';
@ -38,7 +39,8 @@ export default function JavaScriptCodingWorkspaceSection({
studyListKey, studyListKey,
}: Props) { }: Props) {
const { colorScheme } = useColorSchemePreferences(); const { colorScheme } = useColorSchemePreferences();
const bundlerURL = useSandpackBundlerURL(sandpackO11yInstance); const [bundlerURL, changeToFallbackUrl] =
useSandpackBundlerURL(sandpackO11yInstance);
const { files, skeleton, workspace } = question; const { files, skeleton, workspace } = question;
const loadedCode = loadLocalJavaScriptQuestionCode( const loadedCode = loadLocalJavaScriptQuestionCode(
@ -98,6 +100,10 @@ export default function JavaScriptCodingWorkspaceSection({
workspace={workspace} workspace={workspace}
onLanguageChange={onLanguageChange} onLanguageChange={onLanguageChange}
/> />
<SandpackTimeout
instance={sandpackO11yInstance}
onTimeout={changeToFallbackUrl}
/>
<SandpackObservability <SandpackObservability
bundlerURL={bundlerURL} bundlerURL={bundlerURL}
instance={sandpackO11yInstance} instance={sandpackO11yInstance}

View File

@ -15,11 +15,12 @@ import {
questionUserInterfaceDescriptionPath, questionUserInterfaceDescriptionPath,
questionUserInterfaceSolutionPath, questionUserInterfaceSolutionPath,
} from '~/components/interviews/questions/content/user-interface/QuestionUserInterfaceRoutes'; } from '~/components/interviews/questions/content/user-interface/QuestionUserInterfaceRoutes';
import SandpackObservability from '~/components/workspace/common/sandpack/SandpackObservability';
import { SandpackTimeout } from '~/components/workspace/common/sandpack/SandpackTimeout';
import { useSandpackBundlerURL } from '~/components/workspace/common/sandpack/useSandpackBundlerURL'; import { useSandpackBundlerURL } from '~/components/workspace/common/sandpack/useSandpackBundlerURL';
import { useI18nRouter } from '~/next-i18nostic/src'; import { useI18nRouter } from '~/next-i18nostic/src';
import SandpackObservability from '../common/sandpack/SandpackObservability';
import UserInterfaceCodingWorkspace from './UserInterfaceCodingWorkspace'; import UserInterfaceCodingWorkspace from './UserInterfaceCodingWorkspace';
import { UserInterfaceCodingWorkspaceSavesContextProvider } from './UserInterfaceCodingWorkspaceSaveContext'; import { UserInterfaceCodingWorkspaceSavesContextProvider } from './UserInterfaceCodingWorkspaceSaveContext';
@ -44,7 +45,8 @@ export default function UserInterfaceCodingWorkspaceSavesPage({
}: Props) { }: Props) {
const router = useI18nRouter(); const router = useI18nRouter();
const { colorScheme } = useColorSchemePreferences(); const { colorScheme } = useColorSchemePreferences();
const bundlerURL = useSandpackBundlerURL(sandpackO11yInstance); const [bundlerURL, changeToFallbackUrl] =
useSandpackBundlerURL(sandpackO11yInstance);
const { metadata, skeletonBundle } = question; const { metadata, skeletonBundle } = question;
const { files: defaultFiles, workspace } = skeletonBundle; const { files: defaultFiles, workspace } = skeletonBundle;
@ -106,6 +108,10 @@ export default function UserInterfaceCodingWorkspaceSavesPage({
); );
}} }}
/> />
<SandpackTimeout
instance={sandpackO11yInstance}
onTimeout={changeToFallbackUrl}
/>
<SandpackObservability <SandpackObservability
bundlerURL={bundlerURL} bundlerURL={bundlerURL}
instance={sandpackO11yInstance} instance={sandpackO11yInstance}

View File

@ -9,6 +9,7 @@ import type {
QuestionUserInterface, QuestionUserInterface,
} from '~/components/interviews/questions/common/QuestionsTypes'; } from '~/components/interviews/questions/common/QuestionsTypes';
import type { QuestionUserInterfaceMode } from '~/components/interviews/questions/common/QuestionUserInterfacePath'; import type { QuestionUserInterfaceMode } from '~/components/interviews/questions/common/QuestionUserInterfacePath';
import { SandpackTimeout } from '~/components/workspace/common/sandpack/SandpackTimeout';
import { useSandpackBundlerURL } from '~/components/workspace/common/sandpack/useSandpackBundlerURL'; import { useSandpackBundlerURL } from '~/components/workspace/common/sandpack/useSandpackBundlerURL';
import UserInterfaceCodingWorkspace from '~/components/workspace/user-interface/UserInterfaceCodingWorkspace'; import UserInterfaceCodingWorkspace from '~/components/workspace/user-interface/UserInterfaceCodingWorkspace';
import { loadLocalUserInterfaceQuestionCode } from '~/components/workspace/user-interface/UserInterfaceCodingWorkspaceCodeStorage'; import { loadLocalUserInterfaceQuestionCode } from '~/components/workspace/user-interface/UserInterfaceCodingWorkspaceCodeStorage';
@ -44,7 +45,8 @@ export default function UserInterfaceCodingWorkspaceSection({
studyListKey, studyListKey,
}: Props) { }: Props) {
const { colorScheme } = useColorSchemePreferences(); const { colorScheme } = useColorSchemePreferences();
const bundlerURL = useSandpackBundlerURL(sandpackO11yInstance); const [bundlerURL, changeToFallbackUrl] =
useSandpackBundlerURL(sandpackO11yInstance);
const loadedFiles = loadLocalUserInterfaceQuestionCode( const loadedFiles = loadLocalUserInterfaceQuestionCode(
question, question,
@ -105,6 +107,10 @@ export default function UserInterfaceCodingWorkspaceSection({
studyListKey={studyListKey} studyListKey={studyListKey}
onFrameworkChange={onFrameworkChange} onFrameworkChange={onFrameworkChange}
/> />
<SandpackTimeout
instance={sandpackO11yInstance}
onTimeout={changeToFallbackUrl}
/>
<SandpackObservability <SandpackObservability
bundlerURL={bundlerURL} bundlerURL={bundlerURL}
instance={sandpackO11yInstance} instance={sandpackO11yInstance}

View File

@ -9,6 +9,7 @@ import { useIntl } from '~/components/intl';
import Anchor from '~/components/ui/Anchor'; import Anchor from '~/components/ui/Anchor';
import Banner from '~/components/ui/Banner'; import Banner from '~/components/ui/Banner';
import SandpackObservability from '~/components/workspace/common/sandpack/SandpackObservability'; import SandpackObservability from '~/components/workspace/common/sandpack/SandpackObservability';
import { SandpackTimeout } from '~/components/workspace/common/sandpack/SandpackTimeout';
import { useSandpackBundlerURL } from '~/components/workspace/common/sandpack/useSandpackBundlerURL'; import { useSandpackBundlerURL } from '~/components/workspace/common/sandpack/useSandpackBundlerURL';
import UserInterfaceCodingWorkspacePreview from './UserInterfaceCodingWorkspacePreview'; import UserInterfaceCodingWorkspacePreview from './UserInterfaceCodingWorkspacePreview';
@ -25,7 +26,8 @@ export default function UserInterfaceCodingWorkspaceSolutionPreviewTab({
}: Props) { }: Props) {
const intl = useIntl(); const intl = useIntl();
const { colorScheme } = useColorSchemePreferences(); const { colorScheme } = useColorSchemePreferences();
const bundlerURL = useSandpackBundlerURL(sandpackO11yInstance); const [bundlerURL, changeToFallbackUrl] =
useSandpackBundlerURL(sandpackO11yInstance);
const { dispatch, getTabById } = const { dispatch, getTabById } =
useUserInterfaceCodingWorkspaceTilesContext(); useUserInterfaceCodingWorkspaceTilesContext();
@ -88,6 +90,10 @@ export default function UserInterfaceCodingWorkspaceSolutionPreviewTab({
}} }}
theme={colorScheme === 'dark' ? 'dark' : undefined}> theme={colorScheme === 'dark' ? 'dark' : undefined}>
<UserInterfaceCodingWorkspacePreview /> <UserInterfaceCodingWorkspacePreview />
<SandpackTimeout
instance={sandpackO11yInstance}
onTimeout={changeToFallbackUrl}
/>
<SandpackObservability <SandpackObservability
bundlerURL={bundlerURL} bundlerURL={bundlerURL}
instance={sandpackO11yInstance} instance={sandpackO11yInstance}