Skip to content

Commit 76023af

Browse files
authored
Custom Dashboards Versioning fix (#1418)
This PR fixes the versioning error that we ran into for custom dashboards. Now if the latest version of the packages does not work, we fall back to the version that is one patch below the latest version. We log this into sentry. If the fall back doesn't work either, we log that into sentry as well and show the user an error message. Apart from that, I also made changes to ensure dashboards with older versions of the dashboard-ui-component package would still work. Each dashboard now stores the version it was created with, as a comment at the top of its source code, and we use that version when loading the dashboard. When a dashboard gets edited via the AI chat, we re-stamp it with the latest version of the package so it stays up to date. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved error handling and reporting for dashboard load failures; host surfaces structured dependency errors for faster diagnostics. * Added automatic fallback loading for missing resources to reduce load failures. * Fixed page height calculation so pages align correctly with the viewport. * **New Features** * Generated and editor-provided dashboard code is now stamped with the app version for clearer provenance. * **UI/UX Improvements** * Clearer, more informative error messages when custom dashboard loading encounters issues. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1418) <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent c9493e1 commit 76023af

3 files changed

Lines changed: 123 additions & 32 deletions

File tree

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/dashboards/[dashboardId]/page-client.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11

22
"use client";
33

4-
import { DashboardSandboxHost, type DashboardRuntimeError, type WidgetSelection } from "@/components/commands/create-dashboard/dashboard-sandbox-host";
4+
import { DashboardSandboxHost, stampEsmVersion, type DashboardRuntimeError, type WidgetSelection } from "@/components/commands/create-dashboard/dashboard-sandbox-host";
5+
import packageJson from "../../../../../../../../package.json";
56
import { useRouter, useRouterConfirm } from "@/components/router";
67
import { StreamingCodeViewer } from "@/components/streaming-code-viewer";
78
import { ActionDialog, Button, Typography, useToast } from "@/components/ui";
@@ -239,8 +240,9 @@ function DashboardDetailContent({
239240

240241
const handleCodeUpdate = useCallback((toolCall: ToolCallContent) => {
241242
if (typeof toolCall.args.content === "string") {
242-
setPendingCode(toolCall.args.content);
243-
setCurrentTsxSource(toolCall.args.content);
243+
const stamped = stampEsmVersion(toolCall.args.content, packageJson.version);
244+
setPendingCode(stamped);
245+
setCurrentTsxSource(stamped);
244246
clearTimeout(codePhaseTimerRef.current);
245247
setCodePhase("typing");
246248
codePhaseTimerRef.current = setTimeout(() => {
@@ -450,7 +452,7 @@ function DashboardDetailContent({
450452
<AssistantChat
451453
chatAdapter={createDashboardChatAdapter(backendBaseUrl, currentTsxSource, handleCodeUpdate, currentUser, enabledAppIds, projectId, handleRunStart, handleRunEnd)}
452454
historyAdapter={createHistoryAdapter(adminApp, dashboardId)}
453-
toolComponents={<DashboardToolUI setCurrentCode={setCurrentTsxSource} currentCode={currentTsxSource} />}
455+
toolComponents={<DashboardToolUI setCurrentCode={(code) => setCurrentTsxSource(stampEsmVersion(code, packageJson.version))} currentCode={currentTsxSource} />}
454456
useOffWhiteLightMode
455457
composerPlaceholder={currentHasSource ? undefined : DASHBOARD_COMPOSER_PLACEHOLDER}
456458
runningStatusMessages={!isCreating ? UPDATE_STATUS_MESSAGES : undefined}

apps/dashboard/src/components/commands/create-dashboard/create-dashboard-preview.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import { useChat, type UIMessage } from "@ai-sdk/react";
2121
import { convertToModelMessages, DefaultChatTransport } from "ai";
2222
import { memo, useCallback, useMemo, useRef, useState } from "react";
2323
import { CmdKPreviewProps } from "../../cmdk-commands";
24-
import { DashboardSandboxHost } from "./dashboard-sandbox-host";
24+
import { DashboardSandboxHost, stampEsmVersion } from "./dashboard-sandbox-host";
2525
import { StreamingCodeViewer } from "../../streaming-code-viewer";
26+
import packageJson from "../../../../package.json";
2627

2728
type DashboardArtifact = {
2829
prompt: string,
@@ -176,18 +177,19 @@ const CreateDashboardPreviewInner = memo(function CreateDashboardPreviewInner({
176177
phase = "idle";
177178
}
178179

179-
const displayCode = toolPart?.code ?? "";
180+
const displayCode = toolPart?.code ? stampEsmVersion(toolPart.code, packageJson.version) : "";
180181

181182
if (toolPart?.state === "input-available" && !artifact && !finalizedRef.current) {
182183
finalizedRef.current = true;
183184
const sanitized = sanitizeGeneratedCode(toolPart.code);
185+
const stamped = stampEsmVersion(sanitized, packageJson.version);
184186
setArtifact({
185187
prompt,
186188
projectId,
187189
runtimeCodegen: {
188190
title: prompt.slice(0, 120),
189191
description: "",
190-
uiRuntimeSourceCode: sanitized,
192+
uiRuntimeSourceCode: stamped,
191193
},
192194
});
193195
setIframeReady(false);

apps/dashboard/src/components/commands/create-dashboard/dashboard-sandbox-host.tsx

Lines changed: 112 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,57 @@ type DashboardArtifact = {
1818
function html(strings: TemplateStringsArray, ...values: unknown[]): string {
1919
return strings.reduce<string>((result, str, i) => result + str + (values[i] ?? ''), '');
2020
}
21-
2221
const isDev = process.env.NODE_ENV === "development";
2322

23+
function getEsmFallbackVersion(version: string): string {
24+
const parts = version.split(".");
25+
if (parts.length !== 3) return version;
26+
const patch = Number(parts[2]);
27+
if (!Number.isInteger(patch) || patch <= 0) return version;
28+
return `${parts[0]}.${parts[1]}.${patch - 1}`;
29+
}
30+
31+
const ESM_VERSION_HEADER = "// @stack-esm-version:";
32+
const ESM_VERSION_REGEX = /^\/\/\s*@stack-esm-version:\s*(\S+)\s*$/m;
33+
34+
function extractEsmVersion(sourceCode: string): string | null {
35+
const match = sourceCode.match(ESM_VERSION_REGEX);
36+
return match ? match[1] : null;
37+
}
38+
39+
export function stampEsmVersion(sourceCode: string, version: string): string {
40+
if (ESM_VERSION_REGEX.test(sourceCode)) {
41+
return sourceCode.replace(ESM_VERSION_REGEX, `${ESM_VERSION_HEADER} ${version}`);
42+
}
43+
return `${ESM_VERSION_HEADER} ${version}\n${sourceCode}`;
44+
}
45+
2446
function getDependencyScripts(esmVersion: string, esmFallbackVersion: string, dashboardUrl: string): string {
2547
if (isDev) {
2648
return html`
2749
<script type="module">
50+
function formatDependencyError(error) {
51+
return error instanceof Error ? error.message : String(error);
52+
}
53+
54+
function reportDependencyError(message, error) {
55+
window.parent.postMessage({
56+
type: 'dashboard-sandbox-dependency-error',
57+
message,
58+
stack: error instanceof Error ? error.stack : undefined,
59+
}, '*');
60+
}
61+
62+
function failDependencyLoad(message, error) {
63+
reportDependencyError(message, error);
64+
window.__depsError = {
65+
message,
66+
stack: error instanceof Error ? error.stack : undefined,
67+
};
68+
window.__depsReady = true;
69+
window.dispatchEvent(new Event('deps-ready'));
70+
}
71+
2872
import React from 'https://esm.sh/react@19.2.3';
2973
import * as ReactDOM from 'https://esm.sh/react-dom@19.2.3?deps=react@19.2.3';
3074
import * as ReactDOMClient from 'https://esm.sh/react-dom@19.2.3/client?deps=react@19.2.3';
@@ -41,14 +85,14 @@ function getDependencyScripts(esmVersion: string, esmFallbackVersion: string, da
4185
window.StackServerApp = StackSDK.StackServerApp;
4286
window.StackSDK = StackSDK;
4387
} catch (e) {
44-
window.parent.postMessage({ type: 'dashboard-error-boundary', message: '[sandbox] Stack SDK failed at version ${esmVersion}, trying fallback ${esmFallbackVersion}: ' + e?.message }, '*');
88+
reportDependencyError('[sandbox] @stackframe/js failed at version ${esmVersion}; trying fallback ${esmFallbackVersion}: ' + formatDependencyError(e), e);
4589
try {
4690
const StackSDK = await import('https://esm.sh/@stackframe/js@${esmFallbackVersion}');
4791
window.StackAdminApp = StackSDK.StackAdminApp;
4892
window.StackServerApp = StackSDK.StackServerApp;
4993
window.StackSDK = StackSDK;
5094
} catch (e2) {
51-
window.parent.postMessage({ type: 'dashboard-error-boundary', message: '[sandbox] Stack SDK fallback also failed: ' + e2?.message }, '*');
95+
failDependencyLoad('[sandbox] @stackframe/js fallback failed at version ${esmFallbackVersion}: ' + formatDependencyError(e2), e2);
5296
}
5397
}
5498
window.generateUuid = () => crypto.randomUUID();
@@ -61,17 +105,39 @@ function getDependencyScripts(esmVersion: string, esmFallbackVersion: string, da
61105
window.dispatchEvent(new Event('deps-ready'));
62106
};
63107
script.onerror = (e) => {
64-
window.parent.postMessage({
65-
type: 'dashboard-error-boundary',
66-
message: 'Failed to load dashboard-ui-components IIFE bundle',
67-
}, '*');
108+
const message = '[sandbox] Failed to load local dashboard-ui-components IIFE bundle. Run pnpm --filter @stackframe/dashboard-ui-components dev or pnpm --filter @stackframe/dashboard-ui-components build so apps/dashboard/public/dashboard-ui-components.iife.js exists.';
109+
failDependencyLoad(message, e instanceof Error ? e : new Error(message));
68110
};
69111
document.head.appendChild(script);
70112
</script>`;
71113
}
72114

73115
return html`
74116
<script type="module">
117+
const CUSTOM_DASHBOARD_LOAD_ERROR_MESSAGE = 'There was a problem loading custom dashboards. Please refresh the page and try again.';
118+
119+
function formatDependencyError(error) {
120+
return error instanceof Error ? error.message : String(error);
121+
}
122+
123+
function reportDependencyError(message, error) {
124+
window.parent.postMessage({
125+
type: 'dashboard-sandbox-dependency-error',
126+
message,
127+
stack: error instanceof Error ? error.stack : undefined,
128+
}, '*');
129+
}
130+
131+
function failDependencyLoad(message, error) {
132+
reportDependencyError(message, error);
133+
window.__depsError = {
134+
message: CUSTOM_DASHBOARD_LOAD_ERROR_MESSAGE,
135+
stack: error instanceof Error ? error.stack : undefined,
136+
};
137+
window.__depsReady = true;
138+
window.dispatchEvent(new Event('deps-ready'));
139+
}
140+
75141
import React from 'https://esm.sh/react@19.2.3';
76142
import * as ReactDOM from 'https://esm.sh/react-dom@19.2.3?deps=react@19.2.3';
77143
import * as ReactDOMClient from 'https://esm.sh/react-dom@19.2.3/client?deps=react@19.2.3';
@@ -89,21 +155,27 @@ function getDependencyScripts(esmVersion: string, esmFallbackVersion: string, da
89155
import('https://esm.sh/@stackframe/js@${esmVersion}'),
90156
]);
91157
} catch (e) {
92-
window.parent.postMessage({ type: 'dashboard-error-boundary', message: '[sandbox] Failed to load at version ${esmVersion}, trying fallback ${esmFallbackVersion}: ' + e?.message }, '*');
93-
[DashboardUIComponents, StackSDK] = await Promise.all([
94-
import('https://esm.sh/@stackframe/dashboard-ui-components@${esmFallbackVersion}?deps=react@19.2.3,react-dom@19.2.3'),
95-
import('https://esm.sh/@stackframe/js@${esmFallbackVersion}'),
96-
]);
158+
reportDependencyError('[sandbox] Custom dashboard packages failed at version ${esmVersion}; trying fallback ${esmFallbackVersion}: ' + formatDependencyError(e), e);
159+
try {
160+
[DashboardUIComponents, StackSDK] = await Promise.all([
161+
import('https://esm.sh/@stackframe/dashboard-ui-components@${esmFallbackVersion}?deps=react@19.2.3,react-dom@19.2.3'),
162+
import('https://esm.sh/@stackframe/js@${esmFallbackVersion}'),
163+
]);
164+
} catch (e2) {
165+
failDependencyLoad('[sandbox] Custom dashboard package fallback failed at version ${esmFallbackVersion}: ' + formatDependencyError(e2), e2);
166+
}
97167
}
98168
99-
window.DashboardUI = DashboardUIComponents;
100-
window.StackAdminApp = StackSDK.StackAdminApp;
101-
window.StackServerApp = StackSDK.StackServerApp;
102-
window.StackSDK = StackSDK;
103-
window.generateUuid = () => crypto.randomUUID();
169+
if (!window.__depsError) {
170+
window.DashboardUI = DashboardUIComponents;
171+
window.StackAdminApp = StackSDK.StackAdminApp;
172+
window.StackServerApp = StackSDK.StackServerApp;
173+
window.StackSDK = StackSDK;
174+
window.generateUuid = () => crypto.randomUUID();
104175
105-
window.__depsReady = true;
106-
window.dispatchEvent(new Event('deps-ready'));
176+
window.__depsReady = true;
177+
window.dispatchEvent(new Event('deps-ready'));
178+
}
107179
</script>`;
108180
}
109181

@@ -117,8 +189,8 @@ function escapeScriptContent(code: string): string {
117189
function getSandboxDocument(artifact: DashboardArtifact, baseUrl: string, dashboardUrl: string, initialTheme: "light" | "dark", showControls: boolean, initialChatOpen: boolean): string {
118190
const sourceCode = escapeScriptContent(artifact.runtimeCodegen.uiRuntimeSourceCode);
119191
const darkClass = initialTheme === "dark" ? "dark" : "";
120-
const esmVersion = packageJson.version;
121-
const esmFallbackVersion = "2.8.71";
192+
const esmVersion = extractEsmVersion(artifact.runtimeCodegen.uiRuntimeSourceCode) ?? packageJson.version;
193+
const esmFallbackVersion = getEsmFallbackVersion(esmVersion);
122194
const devScriptSrc = isDev ? ` ${dashboardUrl}` : '';
123195
const devConnectSrc = isDev ? ` ${dashboardUrl}` : '';
124196

@@ -307,10 +379,18 @@ function getSandboxDocument(artifact: DashboardArtifact, baseUrl: string, dashbo
307379
};
308380
309381
async function waitForDeps() {
310-
if (window.__depsReady) return;
311-
await new Promise(resolve => {
312-
window.addEventListener('deps-ready', resolve, { once: true });
313-
});
382+
if (!window.__depsReady) {
383+
await new Promise(resolve => {
384+
window.addEventListener('deps-ready', resolve, { once: true });
385+
});
386+
}
387+
if (window.__depsError) {
388+
const error = new Error(window.__depsError.message || 'There was a problem loading custom dashboards. Please refresh the page and try again.');
389+
if (window.__depsError.stack) {
390+
error.stack = window.__depsError.stack;
391+
}
392+
throw error;
393+
}
314394
}
315395
316396
async function requestAccessToken() {
@@ -735,6 +815,13 @@ export const DashboardSandboxHost = memo(function DashboardSandboxHost({
735815
return;
736816
}
737817

818+
if (type === "dashboard-sandbox-dependency-error") {
819+
const err = new Error(event.data.message ?? 'Unknown custom dashboard dependency error');
820+
if (event.data.stack) err.stack = event.data.stack;
821+
captureError('dashboard-sandbox-dependency-error', err);
822+
return;
823+
}
824+
738825
if (type === "dashboard-error-boundary") {
739826
const err = new Error(event.data.message ?? 'Unknown dashboard error');
740827
if (event.data.stack) err.stack = event.data.stack;

0 commit comments

Comments
 (0)