fix(core): implement editor timeout and report error from boundary (#5105) (#5151)

fix(core): implement editor timeout and report error from boundary (#5105)

ci: add sentry env when frontend assets build (#5131)

fix(core): expose catched editor load error (#5133)

fix(infra): use blocksuite api to check compatibility (#5137)

fix(infra): compatibility logic follow blocksuite (#5143)

fix(core): rerender error boundary when route change and improve sentry report (#5147)
This commit is contained in:
Joooye_34
2023-12-01 07:25:08 +00:00
parent 99f98fb9d3
commit eb7d293aaa
42 changed files with 791 additions and 342 deletions

View File

@@ -33,7 +33,7 @@ jobs:
build-core:
name: Build @affine/core
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -50,6 +50,10 @@ jobs:
SHOULD_REPORT_TRACE: true
TRACE_REPORT_ENDPOINT: ${{ secrets.TRACE_REPORT_ENDPOINT }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
- name: Upload core artifact
uses: actions/upload-artifact@v3
with:

View File

@@ -69,8 +69,8 @@ jobs:
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
SKIP_PLUGIN_BUILD: 'true'
SKIP_NX_CACHE: 'true'

View File

@@ -40,6 +40,7 @@ env:
jobs:
before-make:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.build-type || (github.ref_type == 'tag' && contains(github.ref, 'canary') && 'canary') }}
outputs:
RELEASE_VERSION: ${{ steps.get-canary-version.outputs.RELEASE_VERSION }}
steps:
@@ -65,6 +66,7 @@ jobs:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
RELEASE_VERSION: ${{ github.event.inputs.version || steps.get-canary-version.outputs.RELEASE_VERSION }}
SKIP_PLUGIN_BUILD: 'true'
SKIP_NX_CACHE: 'true'

View File

@@ -56,7 +56,7 @@
"env": "SENTRY_AUTH_TOKEN"
},
{
"env": "NEXT_PUBLIC_SENTRY_DSN"
"env": "SENTRY_DSN"
},
{
"env": "DISTRIBUTION"

View File

@@ -4,6 +4,10 @@ import type { createStore, WritableAtom } from 'jotai/vanilla';
import { nanoid } from 'nanoid';
import { migratePages } from '../migration/blocksuite';
import {
checkWorkspaceCompatibility,
MigrationPoint,
} from '../migration/workspace';
export async function initEmptyPage(page: Page, title?: string) {
await page.load(() => {
@@ -244,46 +248,48 @@ export async function buildShowcaseWorkspace(
{} as Record<string, string>
);
});
await Promise.all(
data.map(async ([id, promise, newId]) => {
const { default: template } = await promise;
let json = JSON.stringify(template);
Object.entries(idMap).forEach(([oldId, newId]) => {
json = json.replaceAll(oldId, newId);
});
json = JSON.parse(json);
await workspace
.importPageSnapshot(structuredClone(json), newId)
.catch(error => {
console.error('error importing page', id, error);
});
const page = workspace.getPage(newId);
assertExists(page);
await page.load();
workspace.schema.upgradePage(
0,
{
'affine:note': 1,
'affine:bookmark': 1,
'affine:database': 2,
'affine:divider': 1,
'affine:image': 1,
'affine:list': 1,
'affine:code': 1,
'affine:page': 2,
'affine:paragraph': 1,
'affine:surface': 3,
},
page.spaceDoc
);
// The showcase building will create multiple pages once, and may skip the version writing.
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
if (!workspace.meta.blockVersions) {
await migratePages(workspace.doc, workspace.schema);
}
})
);
// Import page one by one to prevent workspace meta race condition problem.
for (const [id, promise, newId] of data) {
const { default: template } = await promise;
let json = JSON.stringify(template);
Object.entries(idMap).forEach(([oldId, newId]) => {
json = json.replaceAll(oldId, newId);
});
json = JSON.parse(json);
await workspace
.importPageSnapshot(structuredClone(json), newId)
.catch(error => {
console.error('error importing page', id, error);
});
const page = workspace.getPage(newId);
assertExists(page);
await page.load();
workspace.schema.upgradePage(
0,
{
'affine:note': 1,
'affine:bookmark': 1,
'affine:database': 2,
'affine:divider': 1,
'affine:image': 1,
'affine:list': 1,
'affine:code': 1,
'affine:page': 2,
'affine:paragraph': 1,
'affine:surface': 3,
},
page.spaceDoc
);
}
// The showcase building will create multiple pages once, and may skip the version writing.
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
const compatibilityResult = checkWorkspaceCompatibility(workspace);
if (compatibilityResult === MigrationPoint.BlockVersion) {
await migratePages(workspace.doc, workspace.schema);
}
Object.entries(pageMetas).forEach(([oldId, meta]) => {
const newId = idMap[oldId];
workspace.setPageMeta(newId, meta);

View File

@@ -20,13 +20,18 @@ export async function migratePages(
const meta = rootDoc.getMap('meta') as YMap<unknown>;
const versions = meta.get('blockVersions') as YMap<number>;
const oldVersions = versions?.toJSON() ?? {};
spaces.forEach((space: YDoc) => {
try {
schema.upgradePage(0, oldVersions, space);
} catch (e) {
console.error(`page ${space.guid} upgrade failed`, e);
}
schema.upgradePage(0, oldVersions, space);
});
schema.upgradeWorkspace(rootDoc);
// Hard code to upgrade page version to 2.
// Let e2e to ensure the data version is correct.
const pageVersion = meta.get('pageVersion');
if (typeof pageVersion !== 'number' || pageVersion < 2) {
meta.set('pageVersion', 2);
}
const newVersions = getLatestVersions(schema);
meta.set('blockVersions', new YMap(Object.entries(newVersions)));

View File

@@ -43,3 +43,25 @@ export function guidCompatibilityFix(rootDoc: YDoc) {
});
return changed;
}
/**
* Hard code to fix workspace version to be compatible with legacy data.
* Let e2e to ensure the data version is correct.
*/
export function fixWorkspaceVersion(rootDoc: YDoc) {
const meta = rootDoc.getMap('meta') as YMap<unknown>;
/**
* It doesn't matter to upgrade workspace version from 1 or undefined to 2.
* Blocksuite just set the value, do nothing else.
*/
const workspaceVersion = meta.get('workspaceVersion');
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
meta.set('workspaceVersion', 2);
const pageVersion = meta.get('pageVersion');
if (typeof pageVersion !== 'number') {
meta.set('pageVersion', 1);
}
}
}

View File

@@ -58,15 +58,25 @@ export function checkWorkspaceCompatibility(
return MigrationPoint.SubDoc;
}
// Sometimes, blocksuite will not write blockVersions to meta.
// Just fix it when user open the workspace.
const blockVersions = workspace.meta.blockVersions;
if (!blockVersions) {
const hasVersion = workspace.meta.hasVersion;
if (!hasVersion) {
return MigrationPoint.BlockVersion;
}
// TODO: Catch compatibility error from blocksuite to show upgrade page.
// Temporarily follow the check logic of blocksuite.
if ((workspace.meta.pages?.length ?? 0) <= 1) {
try {
workspace.meta.validateVersion(workspace);
} catch (e) {
console.info('validateVersion error', e);
return MigrationPoint.BlockVersion;
}
}
// From v2, we depend on blocksuite to check and migrate data.
for (const [flavour, version] of Object.entries(blockVersions)) {
const blockVersions = workspace.meta.blockVersions;
for (const [flavour, version] of Object.entries(blockVersions ?? {})) {
const schema = workspace.schema.flavourSchemaMap.get(flavour);
if (schema?.version !== version) {
return MigrationPoint.BlockVersion;

View File

@@ -7,14 +7,12 @@ import type { CSSProperties, ReactElement } from 'react';
import {
memo,
Suspense,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
import { type Map as YMap } from 'yjs';
import { Skeleton } from '../../ui/skeleton';
import {
@@ -77,6 +75,67 @@ const useBlockElementById = (
return blockElement;
};
/**
* TODO: Define error to unexpected state together in the future.
*/
export class NoPageRootError extends Error {
constructor(public page: Page) {
super('Page root not found when render editor!');
// Log info to let sentry collect more message
const hasExpectSpace = Array.from(page.doc.spaces.values()).some(
doc => page.spaceDoc.guid === doc.guid
);
const blocks = page.spaceDoc.getMap('blocks') as YMap<YMap<any>>;
const havePageBlock = Array.from(blocks.values()).some(
block => block.get('sys:flavour') === 'affine:page'
);
console.info(
'NoPageRootError current data: %s',
JSON.stringify({
expectPageId: page.id,
expectGuid: page.spaceDoc.guid,
hasExpectSpace,
blockSize: blocks.size,
havePageBlock,
})
);
}
}
/**
* TODO: Defined async cache to support suspense, instead of reflect symbol to provider persistent error cache.
*/
const PAGE_LOAD_KEY = Symbol('PAGE_LOAD');
const PAGE_ROOT_KEY = Symbol('PAGE_ROOT');
function usePageRoot(page: Page) {
let load$ = Reflect.get(page, PAGE_LOAD_KEY);
if (!load$) {
load$ = page.load();
Reflect.set(page, PAGE_LOAD_KEY, load$);
}
use(load$);
if (!page.root) {
let root$: Promise<void> | undefined = Reflect.get(page, PAGE_ROOT_KEY);
if (!root$) {
root$ = new Promise((resolve, reject) => {
const disposable = page.slots.rootAdded.once(() => {
resolve();
});
window.setTimeout(() => {
disposable.dispose();
reject(new NoPageRootError(page));
}, 20 * 1000);
});
Reflect.set(page, PAGE_ROOT_KEY, root$);
}
use(root$);
}
return page.root;
}
const BlockSuiteEditorImpl = ({
mode,
page,
@@ -86,9 +145,8 @@ const BlockSuiteEditorImpl = ({
onModeChange,
style,
}: EditorProps): ReactElement => {
if (!page.loaded) {
use(page.waitForLoaded());
}
usePageRoot(page);
assertExists(page, 'page should not be null');
const editorRef = useRef<EditorContainer | null>(null);
if (editorRef.current === null) {
@@ -176,27 +234,7 @@ const BlockSuiteEditorImpl = ({
);
};
const BlockSuiteErrorFallback = (
props: FallbackProps & ErrorBoundaryProps
): ReactElement => {
return (
<div>
<h1>Sorry.. there was an error</h1>
<div>{props.error.message}</div>
<button
data-testid="error-fallback-reset-button"
onClick={() => {
props.onReset?.();
props.resetErrorBoundary();
}}
>
Try again
</button>
</div>
);
};
export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
export const EditorLoading = memo(function EditorLoading() {
return (
<div className={blockSuiteEditorStyle}>
<Skeleton
@@ -210,21 +248,12 @@ export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
});
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
props: EditorProps & ErrorBoundaryProps
props: EditorProps
): ReactElement {
return (
<ErrorBoundary
fallbackRender={useCallback(
(fallbackProps: FallbackProps) => (
<BlockSuiteErrorFallback {...fallbackProps} onReset={props.onReset} />
),
[props.onReset]
)}
>
<Suspense fallback={<BlockSuiteFallback />}>
<BlockSuiteEditorImpl key={props.page.id} {...props} />
</Suspense>
</ErrorBoundary>
<Suspense fallback={<EditorLoading />}>
<BlockSuiteEditorImpl key={props.page.id} {...props} />
</Suspense>
);
});

View File

@@ -1,4 +1,4 @@
import { BlockSuiteFallback } from '../block-suite-editor';
import { EditorLoading } from '../block-suite-editor';
import {
pageDetailSkeletonStyle,
pageDetailSkeletonTitleStyle,
@@ -8,7 +8,7 @@ export const PageDetailSkeleton = () => {
return (
<div className={pageDetailSkeletonStyle}>
<div className={pageDetailSkeletonTitleStyle} />
<BlockSuiteFallback />
<EditorLoading />
</div>
);
};

View File

@@ -351,6 +351,8 @@ export const createConfiguration: (
'process.env.CAPTCHA_SITE_KEY': JSON.stringify(
process.env.CAPTCHA_SITE_KEY
),
'process.env.SENTRY_DSN': JSON.stringify(process.env.SENTRY_DSN),
'process.env.BUILD_TYPE': JSON.stringify(process.env.BUILD_TYPE),
runtimeConfig: JSON.stringify(runtimeConfig),
}),
new CopyPlugin({

View File

@@ -45,6 +45,8 @@
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@react-hookz/web": "^23.1.0",
"@sentry/integrations": "^7.83.0",
"@sentry/react": "^7.83.0",
"@toeverything/components": "^0.0.46",
"@toeverything/theme": "^0.7.20",
"@vanilla-extract/css": "^1.13.0",

View File

@@ -41,7 +41,7 @@
"env": "SENTRY_AUTH_TOKEN"
},
{
"env": "NEXT_PUBLIC_SENTRY_DSN"
"env": "SENTRY_DSN"
},
{
"env": "DISTRIBUTION"

View File

@@ -6,7 +6,15 @@ import {
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import * as Sentry from '@sentry/react';
import type { createStore } from 'jotai/vanilla';
import { useEffect } from 'react';
import {
createRoutesFromChildren,
matchRoutes,
useLocation,
useNavigationType,
} from 'react-router-dom';
import { WorkspaceAdapters } from '../adapters/workspace';
import { performanceLogger } from '../shared';
@@ -51,6 +59,33 @@ export async function setup(store: ReturnType<typeof createStore>) {
performanceSetupLogger.info('setup global');
setupGlobal();
if (window.SENTRY_RELEASE || environment.isDebug) {
// https://docs.sentry.io/platforms/javascript/guides/react/#configure
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.BUILD_TYPE ?? 'development',
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes
),
}),
new Sentry.Replay(),
],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
tracesSampleRate: 1.0,
});
Sentry.setTags({
appVersion: runtimeConfig.appVersion,
editorVersion: runtimeConfig.editorVersion,
});
}
performanceSetupLogger.info('get root workspace meta');
// do not read `rootWorkspacesMetadataAtom` before migration
await store.get(rootWorkspacesMetadataAtom);

View File

@@ -1,191 +0,0 @@
import type {
QueryParamError,
Unreachable,
WorkspaceNotFoundError,
} from '@affine/env/constant';
import { PageNotFoundError } from '@affine/env/constant';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { Button } from '@toeverything/components/button';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
getCurrentStore,
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react';
import { Provider } from 'jotai/react';
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
import type React from 'react';
import { Component, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import {
RecoverableError,
type SessionFetchErrorRightAfterLoginOrSignUp,
} from '../../unexpected-application-state/errors';
import {
errorDescription,
errorDetailStyle,
errorDivider,
errorImage,
errorLayout,
errorRetryButton,
errorTitle,
} from './affine-error-boundary.css';
import errorBackground from './error-status.assets.svg';
export type AffineErrorBoundaryProps = React.PropsWithChildren & {
height?: number | string;
};
type AffineError =
| QueryParamError
| Unreachable
| WorkspaceNotFoundError
| PageNotFoundError
| Error
| SessionFetchErrorRightAfterLoginOrSignUp;
interface AffineErrorBoundaryState {
error: AffineError | null;
canRetryRecoveredError: boolean;
}
export const DumpInfo = () => {
const location = useLocation();
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const currentPageId = useAtomValue(currentPageIdAtom);
const path = location.pathname;
const query = useParams();
useEffect(() => {
console.info('DumpInfo', {
path,
query,
currentWorkspaceId,
currentPageId,
metadata,
});
}, [path, query, currentWorkspaceId, currentPageId, metadata]);
return null;
};
export class AffineErrorBoundary extends Component<
AffineErrorBoundaryProps,
AffineErrorBoundaryState
> {
override state: AffineErrorBoundaryState = {
error: null,
canRetryRecoveredError: true,
};
private readonly handleRecoverableRetry = () => {
if (this.state.error instanceof RecoverableError) {
if (this.state.error.canRetry()) {
this.state.error.retry();
this.setState({
error: null,
canRetryRecoveredError: this.state.error.canRetry(),
});
} else {
document.location.reload();
}
}
};
private readonly handleRefresh = () => {
this.setState({ error: null });
};
static getDerivedStateFromError(
error: AffineError
): AffineErrorBoundaryState {
return {
error,
canRetryRecoveredError:
error instanceof RecoverableError ? error.canRetry() : true,
};
}
override componentDidCatch(error: AffineError, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
override render(): ReactNode {
if (this.state.error) {
let errorDetail: ReactElement | null = null;
const error = this.state.error;
if (error instanceof PageNotFoundError) {
errorDetail = (
<>
<h1>Sorry.. there was an error</h1>
<>
<span> Page error </span>
<span>
Cannot find page {error.pageId} in workspace{' '}
{error.workspace.id}
</span>
</>
</>
);
} else if (error instanceof RecoverableError) {
const retryButtonDesc = this.state.canRetryRecoveredError
? 'Refetch'
: 'Reload';
errorDetail = (
<>
<h1 className={errorTitle}>Sorry.. there was an error</h1>
<span className={errorDescription}> {error.message} </span>
<span className={errorDescription}>
If you are still experiencing this issue, please{' '}
<a
style={{ color: 'var(--affine-primary-color)' }}
href="https://community.affine.pro"
target="__blank"
>
contact us through the community.
</a>
</span>
<Button
className={errorRetryButton}
onClick={this.handleRecoverableRetry}
type="primary"
>
{retryButtonDesc}
</Button>
</>
);
} else {
errorDetail = (
<>
<h1 className={errorTitle}>Sorry.. there was an error</h1>
<code className={errorDescription}>
{error.message ?? error.toString()}
</code>
<Button
onClick={this.handleRefresh}
className={errorRetryButton}
type="primary"
>
Refresh
</Button>
</>
);
}
return (
<div className={errorLayout} style={{ height: this.props.height }}>
<div className={errorDetailStyle}>{errorDetail}</div>
<span className={errorDivider} />
<div
className={errorImage}
style={{ backgroundImage: `url(${errorBackground})` }}
/>
<Provider key="JotaiProvider" store={getCurrentStore()}>
<DumpInfo />
</Provider>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
export const viewport = style({
height: '100%',
width: '100%',
});

View File

@@ -0,0 +1,53 @@
import { getCurrentStore } from '@toeverything/infra/atom';
import { Provider } from 'jotai/react';
import type { FC } from 'react';
import { useMemo } from 'react';
import * as styles from './affine-error-fallback.css';
import {
ERROR_REFLECT_KEY,
type FallbackProps,
} from './error-basic/fallback-creator';
import { DumpInfo } from './error-basic/info-logger';
import { AnyErrorFallback } from './error-fallbacks/any-error-fallback';
import { NoPageRootFallback } from './error-fallbacks/no-page-root-fallback';
import { PageNotFoundDetail } from './error-fallbacks/page-not-found-fallback';
import { RecoverableErrorFallback } from './error-fallbacks/recoverable-error-fallback';
/**
* Register all fallback components here.
* If have new one just add it to the set.
*/
const fallbacks = new Set([
PageNotFoundDetail,
RecoverableErrorFallback,
NoPageRootFallback,
]);
function getErrorFallbackComponent(error: any): FC<FallbackProps> {
for (const Component of fallbacks) {
const ErrorConstructor = Reflect.get(Component, ERROR_REFLECT_KEY);
if (ErrorConstructor && error instanceof ErrorConstructor) {
return Component as FC<FallbackProps>;
}
}
return AnyErrorFallback;
}
export interface AffineErrorFallbackProps extends FallbackProps {
height?: number | string;
}
export const AffineErrorFallback: FC<AffineErrorFallbackProps> = props => {
const { error, resetError, height } = props;
const Component = useMemo(() => getErrorFallbackComponent(error), [error]);
return (
<div className={styles.viewport} style={{ height }}>
<Component error={error} resetError={resetError} />
<Provider key="JotaiProvider" store={getCurrentStore()}>
<DumpInfo error={error} />
</Provider>
</div>
);
};

View File

@@ -0,0 +1,43 @@
<svg width="490" height="242" viewBox="0 0 490 242" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5098 155.545C16.4625 163.027 18.0723 169.655 21.3393 175.432C24.6064 181.208 29.1282 185.73 34.9047 188.997C40.6812 192.264 47.3337 193.898 54.8621 193.898C63.2428 193.898 70.6765 191.838 77.1632 187.719C83.65 183.599 88.7399 177.989 92.4331 170.886C96.1263 163.784 97.9965 155.735 98.0439 146.739C98.0912 137.364 96.1026 129.196 92.078 122.236C88.1007 115.228 82.7977 109.807 76.1689 105.972C69.5875 102.089 62.3905 100.148 54.578 100.148C48.7541 100.148 43.4274 101.166 38.5979 103.202C33.7683 105.19 29.5306 107.866 25.8848 111.227H25.3166L32.703 51H93.2143" stroke="black" stroke-width="2"/>
<ellipse cx="94.4026" cy="50.8894" rx="3.89381" ry="3.88938" fill="#121212"/>
<ellipse cx="16.4026" cy="155.889" rx="3.89381" ry="3.88938" fill="#121212"/>
<ellipse cx="32.4026" cy="50.8894" rx="3.89381" ry="3.88938" fill="#121212"/>
<ellipse cx="25.4026" cy="110.889" rx="3.89381" ry="3.88938" fill="#121212"/>
<path d="M151.592 59.1235L275.951 183.342" stroke="#E3E2E4"/>
<rect x="151.626" y="59.1582" width="124.291" height="124.149" stroke="#E3E2E4"/>
<path d="M275.573 121.465C276.129 117.824 276.417 114.095 276.417 110.299C276.417 69.7024 243.469 36.792 202.826 36.792C162.183 36.792 129.235 69.7024 129.235 110.299C129.235 150.897 162.183 183.807 202.826 183.807C206.465 183.807 210.041 183.543 213.539 183.034" stroke="#E3E2E4"/>
<path d="M213.539 182.964C217.184 183.519 220.917 183.807 224.717 183.807C265.36 183.807 298.308 150.897 298.308 110.299C298.308 69.7024 265.36 36.792 224.717 36.792C184.074 36.792 151.126 69.7024 151.126 110.299C151.126 114.095 151.414 117.824 151.97 121.465" stroke="#E3E2E4"/>
<path d="M151.434 121.465C150.924 124.958 150.66 128.531 150.66 132.166C150.66 172.763 183.608 205.673 224.251 205.673C264.894 205.673 297.842 172.763 297.842 132.166C297.842 91.5686 264.894 58.6582 224.251 58.6582C220.613 58.6582 217.036 58.922 213.539 59.4314" stroke="#E3E2E4"/>
<path d="M213.539 59.4314C210.041 58.922 206.465 58.6582 202.826 58.6582C162.183 58.6582 129.235 91.5686 129.235 132.166C129.235 172.763 162.183 205.673 202.826 205.673C243.469 205.673 276.417 172.763 276.417 132.166C276.417 128.531 276.153 124.958 275.643 121.465" stroke="#E3E2E4"/>
<path d="M275.951 59.1235L151.592 183.342" stroke="#E3E2E4"/>
<path d="M151.126 121.465H275.951" stroke="#E3E2E4"/>
<path d="M213.539 58.6582V183.807" stroke="#E3E2E4"/>
<ellipse cx="275.951" cy="121.465" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="275.951" cy="59.1233" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="275.951" cy="183.807" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="151.592" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="213.539" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="213.539" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="151.592" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="151.592" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="213.539" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
<path d="M338.583 59.1235L462.943 183.342" stroke="#E3E2E4"/>
<rect x="338.617" y="59.1582" width="124.291" height="124.149" stroke="#E3E2E4"/>
<path d="M462.565 121.465C463.12 117.824 463.408 114.095 463.408 110.299C463.408 69.7024 430.46 36.792 389.817 36.792C349.174 36.792 316.226 69.7024 316.226 110.299C316.226 150.897 349.174 183.807 389.817 183.807C393.456 183.807 397.033 183.543 400.53 183.034" stroke="#E3E2E4"/>
<path d="M400.53 182.964C404.175 183.519 407.908 183.807 411.708 183.807C452.351 183.807 485.299 150.897 485.299 110.299C485.299 69.7024 452.351 36.792 411.708 36.792C371.065 36.792 338.117 69.7024 338.117 110.299C338.117 114.095 338.405 117.824 338.961 121.465" stroke="#E3E2E4"/>
<path d="M338.425 121.465C337.915 124.958 337.651 128.531 337.651 132.166C337.651 172.763 370.599 205.673 411.242 205.673C451.886 205.673 484.833 172.763 484.833 132.166C484.833 91.5686 451.886 58.6582 411.242 58.6582C407.604 58.6582 404.027 58.922 400.53 59.4314" stroke="#E3E2E4"/>
<path d="M400.53 59.4314C397.033 58.922 393.456 58.6582 389.817 58.6582C349.174 58.6582 316.226 91.5686 316.226 132.166C316.226 172.763 349.174 205.673 389.817 205.673C430.46 205.673 463.408 172.763 463.408 132.166C463.408 128.531 463.144 124.958 462.634 121.465" stroke="#E3E2E4"/>
<path d="M462.943 59.1235L338.583 183.342" stroke="#E3E2E4"/>
<path d="M338.117 121.465H462.943" stroke="#E3E2E4"/>
<path d="M400.53 58.6582V183.807" stroke="#E3E2E4"/>
<ellipse cx="462.942" cy="121.465" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="462.942" cy="59.1233" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="462.942" cy="183.807" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="338.583" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="400.53" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="400.53" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="338.583" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="338.583" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="400.53" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -6,6 +6,7 @@ export const errorLayout = style({
alignItems: 'center',
height: '100%',
width: '100%',
gap: '20px',
});
export const errorDetailStyle = style({
@@ -24,15 +25,15 @@ export const errorImage = style({
height: '178px',
maxWidth: '400px',
flexGrow: 1,
backgroundSize: 'cover',
});
export const errorDescription = style({
marginTop: '24px',
});
export const errorRetryButton = style({
export const errorFooter = style({
marginTop: '24px',
width: '94px',
});
export const errorDivider = style({

View File

@@ -0,0 +1,106 @@
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import {
type FC,
type PropsWithChildren,
type ReactNode,
useState,
} from 'react';
import imageUrlFor404 from '../error-assets/404-status.assets.svg';
import imageUrlFor500 from '../error-assets/500-status.assets.svg';
import * as styles from './error-detail.css';
export enum ErrorStatus {
NotFound = 404,
Unexpected = 500,
}
export interface ErrorDetailProps extends PropsWithChildren {
status?: ErrorStatus;
direction?: 'column' | 'row';
title: string;
description: ReactNode | Array<ReactNode>;
buttonText?: string;
onButtonClick?: () => void | Promise<void>;
resetError?: () => void;
withoutImage?: boolean;
}
const imageMap = new Map([
[ErrorStatus.NotFound, imageUrlFor404],
[ErrorStatus.Unexpected, imageUrlFor500],
]);
/**
* TODO: Unify with NotFoundPage.
*/
export const ErrorDetail: FC<ErrorDetailProps> = props => {
const {
status = ErrorStatus.Unexpected,
direction = 'row',
description,
onButtonClick,
resetError,
withoutImage,
} = props;
const descriptions = Array.isArray(description) ? description : [description];
const [isBtnLoading, setBtnLoading] = useState(false);
const t = useAFFiNEI18N();
const onBtnClick = useAsyncCallback(async () => {
try {
setBtnLoading(true);
await onButtonClick?.();
resetError?.(); // Only reset when retry success.
} finally {
setBtnLoading(false);
}
}, [onButtonClick, resetError]);
return (
<div className={styles.errorLayout} style={{ flexDirection: direction }}>
<div className={styles.errorDetailStyle}>
<h1 className={styles.errorTitle}>{props.title}</h1>
{descriptions.map((item, i) => (
<p key={i} className={styles.errorDescription}>
{item}
</p>
))}
<div className={styles.errorFooter}>
<Button
type="primary"
onClick={onBtnClick}
loading={isBtnLoading}
size="extraLarge"
>
{props.buttonText ?? t['com.affine.error.retry']()}
</Button>
</div>
</div>
{withoutImage ? null : (
<div
className={styles.errorImage}
style={{ backgroundImage: `url(${imageMap.get(status)})` }}
/>
)}
</div>
);
};
export function ContactUS() {
return (
<Trans>
If you are still experiencing this issue, please{' '}
<a
style={{ color: 'var(--affine-primary-color)' }}
href="https://community.affine.pro"
target="__blank"
>
contact us through the community.
</a>
</Trans>
);
}

View File

@@ -0,0 +1,16 @@
import type { FC } from 'react';
export interface FallbackProps<T extends Error = Error> {
error: T;
resetError: () => void;
}
export const ERROR_REFLECT_KEY = Symbol('ERROR_REFLECT_KEY');
export function createErrorFallback<T extends Error>(
ErrorConstructor: abstract new (...args: any[]) => T,
Component: FC<FallbackProps<T>>
): FC<FallbackProps<T>> {
Reflect.set(Component, ERROR_REFLECT_KEY, ErrorConstructor);
return Component;
}

View File

@@ -0,0 +1,31 @@
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react';
import { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
export interface DumpInfoProps {
error: any;
}
export const DumpInfo = (_props: DumpInfoProps) => {
const location = useLocation();
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const currentPageId = useAtomValue(currentPageIdAtom);
const path = location.pathname;
const query = useParams();
useEffect(() => {
console.info('DumpInfo', {
path,
query,
currentWorkspaceId,
currentPageId,
metadata,
});
}, [path, query, currentWorkspaceId, currentPageId, metadata]);
return null;
};

View File

@@ -0,0 +1,26 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { type FC, useCallback } from 'react';
import { ErrorDetail } from '../error-basic/error-detail';
import type { FallbackProps } from '../error-basic/fallback-creator';
/**
* TODO: Support reload and retry two reset actions in page error and area error.
*/
export const AnyErrorFallback: FC<FallbackProps> = props => {
const { error } = props;
const t = useAFFiNEI18N();
const reloadPage = useCallback(() => {
document.location.reload();
}, []);
return (
<ErrorDetail
title={t['com.affine.error.unexpected-error.title']()}
resetError={reloadPage}
buttonText={t['com.affine.error.reload']()}
description={error.message ?? error.toString()}
/>
);
};

View File

@@ -0,0 +1,21 @@
import { NoPageRootError } from '@affine/component/block-suite-editor';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ContactUS, ErrorDetail } from '../error-basic/error-detail';
import { createErrorFallback } from '../error-basic/fallback-creator';
export const NoPageRootFallback = createErrorFallback(
NoPageRootError,
props => {
const { resetError } = props;
const t = useAFFiNEI18N();
return (
<ErrorDetail
title={t['com.affine.error.no-page-root.title']()}
description={<ContactUS />}
resetError={resetError}
/>
);
}
);

View File

@@ -0,0 +1,30 @@
import { PageNotFoundError } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useCallback } from 'react';
import {
RouteLogic,
useNavigateHelper,
} from '../../../../hooks/use-navigate-helper';
import { ErrorDetail, ErrorStatus } from '../error-basic/error-detail';
import { createErrorFallback } from '../error-basic/fallback-creator';
export const PageNotFoundDetail = createErrorFallback(PageNotFoundError, () => {
const t = useAFFiNEI18N();
const { jumpToIndex } = useNavigateHelper();
const onBtnClick = useCallback(
() => jumpToIndex(RouteLogic.REPLACE),
[jumpToIndex]
);
return (
<ErrorDetail
title={t['com.affine.notFoundPage.title']()}
description={t['404.hint']()}
buttonText={t['404.back']()}
onButtonClick={onBtnClick}
status={ErrorStatus.NotFound}
/>
);
});

View File

@@ -0,0 +1,41 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useCallback, useMemo, useState } from 'react';
import { RecoverableError } from '../../../../unexpected-application-state/errors';
import { ContactUS, ErrorDetail } from '../error-basic/error-detail';
import { createErrorFallback } from '../error-basic/fallback-creator';
export const RecoverableErrorFallback = createErrorFallback(
RecoverableError,
props => {
const { error, resetError } = props;
const t = useAFFiNEI18N();
const [count, rerender] = useState(0);
const canRetry = error.canRetry();
const buttonDesc = useMemo(() => {
if (canRetry) {
return t['com.affine.error.refetch']();
}
return t['com.affine.error.reload']();
}, [canRetry, t]);
const onRetry = useCallback(async () => {
if (canRetry) {
rerender(count + 1);
await error.retry();
} else {
document.location.reload();
}
}, [error, count, canRetry]);
return (
<ErrorDetail
title={t['com.affine.error.unexpected-error.title']()}
resetError={resetError}
buttonText={buttonDesc}
onButtonClick={onRetry}
description={[error.message, <ContactUS key="contact-us" />]}
/>
);
}
);

View File

@@ -0,0 +1,25 @@
<svg width="402" height="178" viewBox="0 0 402 178" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M93.7434 129.308H1L71.7965 15.1021V167.142" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
<ellipse cx="71.4426" cy="14.7483" rx="3.89381" ry="3.88938" fill="#121212" />
<ellipse cx="93.3894" cy="129.308" rx="3.89381" ry="3.88938" fill="#121212" />
<path d="M140.357 27.1235L264.717 151.342" stroke="#E3E2E4" />
<rect x="140.392" y="27.1582" width="124.291" height="124.149" stroke="#E3E2E4" />
<path d="M264.339 89.4652C264.895 85.8242 265.183 82.0954 265.183 78.2995C265.183 37.7024 232.235 4.79199 191.592 4.79199C150.948 4.79199 118 37.7024 118 78.2995C118 118.897 150.948 151.807 191.592 151.807C195.23 151.807 198.807 151.543 202.304 151.034" stroke="#E3E2E4" />
<path d="M202.304 150.964C205.949 151.519 209.682 151.807 213.483 151.807C254.126 151.807 287.074 118.897 287.074 78.2995C287.074 37.7024 254.126 4.79199 213.483 4.79199C172.839 4.79199 139.892 37.7024 139.892 78.2995C139.892 82.0955 140.18 85.8242 140.735 89.4652" stroke="#E3E2E4" />
<path d="M140.2 89.4652C139.69 92.9584 139.426 96.5312 139.426 100.166C139.426 140.763 172.374 173.673 213.017 173.673C253.66 173.673 286.608 140.763 286.608 100.166C286.608 59.5686 253.66 26.6582 213.017 26.6582C209.378 26.6582 205.801 26.922 202.304 27.4314" stroke="#E3E2E4" />
<path d="M202.304 27.4314C198.807 26.922 195.23 26.6582 191.592 26.6582C150.948 26.6582 118 59.5686 118 100.166C118 140.763 150.948 173.673 191.592 173.673C232.235 173.673 265.183 140.763 265.183 100.166C265.183 96.5312 264.919 92.9584 264.409 89.4652" stroke="#E3E2E4" />
<path d="M264.717 27.1235L140.357 151.342" stroke="#E3E2E4" />
<path d="M139.892 89.4653H264.717" stroke="#E3E2E4" />
<path d="M202.304 26.6582V151.807" stroke="#E3E2E4" />
<ellipse cx="264.717" cy="89.4651" rx="3.72614" ry="3.7219" fill="#121212" />
<ellipse cx="264.717" cy="27.1233" rx="3.72614" ry="3.7219" fill="#121212" />
<ellipse cx="264.717" cy="151.807" rx="3.72614" ry="3.7219" fill="#121212" />
<ellipse cx="140.357" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="202.304" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="202.304" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="140.357" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="140.357" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="202.304" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
<path d="M401 127.187H308.257L379.053 12.9805V165.02" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
<ellipse cx="379.407" cy="127.187" rx="3.89381" ry="3.88938" fill="#121212" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,34 @@
import { ErrorBoundary } from '@sentry/react';
import type { FC, PropsWithChildren } from 'react';
import { useCallback } from 'react';
import { AffineErrorFallback } from './affine-error-fallback';
import { type FallbackProps } from './error-basic/fallback-creator';
export { type FallbackProps } from './error-basic/fallback-creator';
export interface AffineErrorBoundaryProps extends PropsWithChildren {
height?: number | string;
}
/**
* TODO: Unify with SWRErrorBoundary
*/
export const AffineErrorBoundary: FC<AffineErrorBoundaryProps> = props => {
const fallbackRender = useCallback(
(fallbackProps: FallbackProps) => {
return <AffineErrorFallback {...fallbackProps} height={props.height} />;
},
[props.height]
);
const onError = useCallback((error: Error, componentStack: string) => {
console.error('Uncaught error:', error, componentStack);
}, []);
return (
<ErrorBoundary fallback={fallbackRender} onError={onError}>
{props.children}
</ErrorBoundary>
);
};

View File

@@ -1,11 +0,0 @@
import type { ReactElement } from 'react';
import type { FallbackProps } from 'react-error-boundary';
export const AnyErrorBoundary = (props: FallbackProps): ReactElement => {
return (
<div>
<p>Something went wrong:</p>
<p>{props.error.toString()}</p>
</div>
);
};

View File

@@ -29,7 +29,6 @@ import {
useRef,
useState,
} from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { openSettingModalAtom } from '../../../atoms';
import type { CheckedUser } from '../../../hooks/affine/use-current-user';
@@ -39,7 +38,7 @@ import { useMemberCount } from '../../../hooks/affine/use-member-count';
import { type Member, useMembers } from '../../../hooks/affine/use-members';
import { useRevokeMemberPermission } from '../../../hooks/affine/use-revoke-member-permission';
import { useUserSubscription } from '../../../hooks/use-subscription';
import { AnyErrorBoundary } from '../any-error-boundary';
import { AffineErrorBoundary } from '../affine-error-boundary';
import * as style from './style.css';
import type { WorkspaceSettingDetailProps } from './types';
@@ -362,10 +361,10 @@ export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
return <MembersPanelLocal />;
}
return (
<ErrorBoundary FallbackComponent={AnyErrorBoundary}>
<AffineErrorBoundary>
<Suspense>
<CloudWorkspaceMembersPanel {...props} />
</Suspense>
</ErrorBoundary>
</AffineErrorBoundary>
);
};

View File

@@ -1,7 +1,7 @@
import { Scrollable } from '@affine/component';
import {
BlockSuiteEditor,
BlockSuiteFallback,
EditorLoading,
} from '@affine/component/block-suite-editor';
import type { PageMode } from '@affine/core/atoms';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
@@ -147,7 +147,7 @@ const HistoryEditorPreview = ({
onModeChange={onModeChange}
/>
) : (
<BlockSuiteFallback />
<EditorLoading />
)}
</div>
);
@@ -410,7 +410,7 @@ export const PageHistoryModal = ({
return (
<ModalContainer onOpenChange={onOpenChange} open={open}>
<Suspense fallback={<BlockSuiteFallback />}>
<Suspense fallback={<EditorLoading />}>
<PageHistoryManager
onClose={onClose}
pageId={pageId}

View File

@@ -19,6 +19,7 @@ export const upgradeTips = style({
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '20px',
textAlign: 'center',
});
const rotate = keyframes({

View File

@@ -44,7 +44,7 @@ interface WorkspaceUpgradeProps {
export const WorkspaceUpgrade = function WorkspaceUpgrade(
props: WorkspaceUpgradeProps
) {
const [upgradeState, , upgradeWorkspace, newWorkspaceId] =
const [upgradeState, error, upgradeWorkspace, newWorkspaceId] =
useUpgradeWorkspace(props.migration);
const t = useAFFiNEI18N();
@@ -75,7 +75,7 @@ export const WorkspaceUpgrade = function WorkspaceUpgrade(
<div className={styles.upgradeBox}>
<AffineShapeIcon width={180} height={180} />
<p className={styles.upgradeTips}>
{t[UPGRADE_TIPS_KEYS[upgradeState]]()}
{error ? error.message : t[UPGRADE_TIPS_KEYS[upgradeState]]()}
</p>
<Button
data-testid="upgrade-workspace-button"

View File

@@ -113,7 +113,6 @@ export const DetailPage = (): ReactElement => {
const currentSyncEngineStatus = useCurrentSyncEngineStatus();
const currentPageId = useAtomValue(currentPageIdAtom);
const [page, setPage] = useState<Page | null>(null);
const [pageLoaded, setPageLoaded] = useState<boolean>(false);
// load page by current page id
useEffect(() => {
@@ -158,30 +157,7 @@ export const DetailPage = (): ReactElement => {
return;
}, [currentSyncEngineStatus, navigate, page]);
// wait for page to be loaded
useEffect(() => {
if (page) {
if (!page.isEmpty) {
setPageLoaded(true);
} else {
setPageLoaded(false);
// call waitForLoaded to trigger load
page
.load(() => {})
.catch(() => {
// do nothing
});
return page.slots.ready.on(() => {
setPageLoaded(true);
}).dispose;
}
} else {
setPageLoaded(false);
}
return;
}, [page]);
if (!currentPageId || !page || !pageLoaded) {
if (!currentPageId || !page) {
return <PageDetailSkeleton key="current-page-is-null" />;
}
@@ -218,8 +194,9 @@ export const Component = () => {
}
}, [params, setContentLayout, setCurrentPageId, setCurrentWorkspaceId]);
// Add a key to force rerender when page changed, to avoid error boundary persisting.
return (
<AffineErrorBoundary>
<AffineErrorBoundary key={params.pageId}>
<DetailPage />
</AffineErrorBoundary>
);

View File

@@ -8,6 +8,7 @@ import {
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
import {
checkWorkspaceCompatibility,
fixWorkspaceVersion,
guidCompatibilityFix,
} from '@toeverything/infra/blocksuite';
import { useSetAtom } from 'jotai';
@@ -54,6 +55,7 @@ export const loader: LoaderFunction = async args => {
workspaceLoaderLogger.info('workspace loaded');
guidCompatibilityFix(workspace.doc);
fixWorkspaceVersion(workspace.doc);
return checkWorkspaceCompatibility(workspace);
};
@@ -73,7 +75,7 @@ export const Component = (): ReactElement => {
const migration = useLoaderData() as MigrationPoint | undefined;
return (
<AffineErrorBoundary height="100vh">
<AffineErrorBoundary key={params.workspaceId} height="100vh">
<WorkspaceLayout migration={migration}>
<Outlet />
</WorkspaceLayout>

View File

@@ -1,5 +1,6 @@
import * as Sentry from '@sentry/react';
import type { RouteObject } from 'react-router-dom';
import { createBrowserRouter } from 'react-router-dom';
import { createBrowserRouter as reactRouterCreateBrowserRouter } from 'react-router-dom';
export const routes = [
{
@@ -70,6 +71,9 @@ export const routes = [
},
] satisfies [RouteObject, ...RouteObject[]];
const createBrowserRouter = Sentry.wrapCreateBrowserRouter(
reactRouterCreateBrowserRouter
);
export const router = createBrowserRouter(routes, {
future: {
v7_normalizeFormMethod: true,

View File

@@ -5,7 +5,7 @@ export abstract class RecoverableError extends Error {
return this.ttl > 0;
}
abstract retry(): void;
abstract retry(): void | Promise<void>;
}
// the first session request failed after login or signup succeed.
@@ -24,8 +24,6 @@ export class SessionFetchErrorRightAfterLoginOrSignUp extends RecoverableError {
}
try {
this.onRetry();
} catch (e) {
console.error('Retry error', e);
} finally {
this.ttl--;
}

View File

@@ -689,7 +689,7 @@
"com.affine.new_edgeless": "New Edgeless",
"com.affine.new_import": "Import",
"com.affine.notFoundPage.backButton": "Back Home",
"com.affine.notFoundPage.title": "404 - Page Not Found",
"com.affine.notFoundPage.title": "Page Not Found",
"com.affine.onboarding.title1": "Hyper merged whiteboard and docs",
"com.affine.onboarding.title2": "Intuitive & robust block-based editing",
"com.affine.onboarding.videoDescription1": "Easily switch between Page mode for structured document creation and Whiteboard mode for the freeform visual expression of creative ideas.",
@@ -963,6 +963,13 @@
"com.affine.workspaceType.offline": "Available Offline",
"com.affine.write_with_a_blank_page": "Write with a blank page",
"com.affine.yesterday": "Yesterday",
"com.affine.error.retry": "Refresh",
"com.affine.error.refetch": "Refetch",
"com.affine.error.reload": "Reload",
"com.affine.error.page-not-found.title": "Refresh",
"com.affine.error.unexpected-error.title": "Something is wrong...",
"com.affine.error.contact.description": "If you are still experiencing this issue, please <1>contact us through the community.</1>",
"com.affine.error.no-page-root.title": "Page content is missing",
"core": "core",
"dark": "Dark",
"emptyAllPages": "Click on the <1>$t(New Page)</1> button to create your first page.",

View File

@@ -31,7 +31,7 @@
"env": "SENTRY_AUTH_TOKEN"
},
{
"env": "NEXT_PUBLIC_SENTRY_DSN"
"env": "SENTRY_DSN"
},
{
"env": "DISTRIBUTION"

View File

@@ -56,6 +56,11 @@ declare global {
var runtimeConfig: RuntimeConfig;
// eslint-disable-next-line no-var
var $AFFINE_SETUP: boolean | undefined;
/**
* Inject by https://www.npmjs.com/package/@sentry/webpack-plugin
*/
// eslint-disable-next-line no-var
var SENTRY_RELEASE: { id: string } | undefined;
}
declare module '@blocksuite/store' {

110
yarn.lock
View File

@@ -357,6 +357,8 @@ __metadata:
"@radix-ui/react-scroll-area": "npm:^1.0.5"
"@radix-ui/react-select": "npm:^2.0.0"
"@react-hookz/web": "npm:^23.1.0"
"@sentry/integrations": "npm:^7.83.0"
"@sentry/react": "npm:^7.83.0"
"@sentry/webpack-plugin": "npm:^2.8.0"
"@svgr/webpack": "npm:^8.1.0"
"@swc/core": "npm:^1.3.93"
@@ -10850,6 +10852,30 @@ __metadata:
languageName: node
linkType: hard
"@sentry-internal/tracing@npm:7.83.0":
version: 7.83.0
resolution: "@sentry-internal/tracing@npm:7.83.0"
dependencies:
"@sentry/core": "npm:7.83.0"
"@sentry/types": "npm:7.83.0"
"@sentry/utils": "npm:7.83.0"
checksum: 7f5d2a2490f15b907c57fbc96ad3fa34a63f585433e0dfefa82400d363004ddff1e5e3b5c40436679b80a8f2afb7cb18f2802d7134ae44f607f0104a352811c3
languageName: node
linkType: hard
"@sentry/browser@npm:7.83.0":
version: 7.83.0
resolution: "@sentry/browser@npm:7.83.0"
dependencies:
"@sentry-internal/tracing": "npm:7.83.0"
"@sentry/core": "npm:7.83.0"
"@sentry/replay": "npm:7.83.0"
"@sentry/types": "npm:7.83.0"
"@sentry/utils": "npm:7.83.0"
checksum: a4d2181dc36d7946b3af133bf9249be3bb3bd6d3eb34c70352c42445416934a98cb537ed7b827b77277ce60cb902e81aa86b5d94bf8de036aedf1f54d301ba78
languageName: node
linkType: hard
"@sentry/bundler-plugin-core@npm:2.10.1":
version: 2.10.1
resolution: "@sentry/bundler-plugin-core@npm:2.10.1"
@@ -10891,6 +10917,28 @@ __metadata:
languageName: node
linkType: hard
"@sentry/core@npm:7.83.0":
version: 7.83.0
resolution: "@sentry/core@npm:7.83.0"
dependencies:
"@sentry/types": "npm:7.83.0"
"@sentry/utils": "npm:7.83.0"
checksum: c75cca15b750180d73975309d44ff3309a369b60f95c1c01f679b868a3eca43ace768d026aa11333c7cc1bc147a947f5f2b507ec72803033e4e3f87ea895cf7b
languageName: node
linkType: hard
"@sentry/integrations@npm:^7.83.0":
version: 7.83.0
resolution: "@sentry/integrations@npm:7.83.0"
dependencies:
"@sentry/core": "npm:7.83.0"
"@sentry/types": "npm:7.83.0"
"@sentry/utils": "npm:7.83.0"
localforage: "npm:^1.8.1"
checksum: 6f614f4f5bb56cdfa7e493b2546ddbd9a45a867fd61e0cf46f5c5af22b74f82b6a8a50c86bab76c9c828c03242652eef72d24a4235c2584e7a0fac58821b7c3d
languageName: node
linkType: hard
"@sentry/node@npm:^7.60.0":
version: 7.81.1
resolution: "@sentry/node@npm:7.81.1"
@@ -10904,6 +10952,32 @@ __metadata:
languageName: node
linkType: hard
"@sentry/react@npm:^7.83.0":
version: 7.83.0
resolution: "@sentry/react@npm:7.83.0"
dependencies:
"@sentry/browser": "npm:7.83.0"
"@sentry/types": "npm:7.83.0"
"@sentry/utils": "npm:7.83.0"
hoist-non-react-statics: "npm:^3.3.2"
peerDependencies:
react: 15.x || 16.x || 17.x || 18.x
checksum: 7668d087914c390a30cc8c6e252c96ab8a47e660580224dbceb48179c83565208ce3dc50ed079366047ec1875f21175d55c73d3e623f01614dcd114d29d0c043
languageName: node
linkType: hard
"@sentry/replay@npm:7.83.0":
version: 7.83.0
resolution: "@sentry/replay@npm:7.83.0"
dependencies:
"@sentry-internal/tracing": "npm:7.83.0"
"@sentry/core": "npm:7.83.0"
"@sentry/types": "npm:7.83.0"
"@sentry/utils": "npm:7.83.0"
checksum: 6e2f1960db208c0723537ffcdc8a7370d97ac2df2103d18ecf824c9b69ac83d76ebf29d37833d7a45ef435f480f0553da7148682d8110031074dd7b3c4936ca7
languageName: node
linkType: hard
"@sentry/types@npm:7.81.1":
version: 7.81.1
resolution: "@sentry/types@npm:7.81.1"
@@ -10911,7 +10985,14 @@ __metadata:
languageName: node
linkType: hard
"@sentry/utils@npm:7.81.1, @sentry/utils@npm:^7.60.0":
"@sentry/types@npm:7.83.0":
version: 7.83.0
resolution: "@sentry/types@npm:7.83.0"
checksum: 257b37678c7ea8624d4cdd9a18ccacbdb53a84be90ab6c0f3a6d6d30e8bc021f72a02a09dcf17ba6b1a9f6797580b89b827a3fcd0bc851185e7fc695ae68fbc3
languageName: node
linkType: hard
"@sentry/utils@npm:7.81.1":
version: 7.81.1
resolution: "@sentry/utils@npm:7.81.1"
dependencies:
@@ -10920,6 +11001,15 @@ __metadata:
languageName: node
linkType: hard
"@sentry/utils@npm:7.83.0, @sentry/utils@npm:^7.60.0":
version: 7.83.0
resolution: "@sentry/utils@npm:7.83.0"
dependencies:
"@sentry/types": "npm:7.83.0"
checksum: f0e4ef51f32e610ec8a33a43566e6f900e88e5f6c31aaf9a325f1b44a65ec882fa6e46122e23451afb0f2cf8f77bd766cc9684f9e40730c7193814d01d94daa4
languageName: node
linkType: hard
"@sentry/webpack-plugin@npm:^2.8.0":
version: 2.10.1
resolution: "@sentry/webpack-plugin@npm:2.10.1"
@@ -25156,6 +25246,15 @@ __metadata:
languageName: node
linkType: hard
"lie@npm:3.1.1":
version: 3.1.1
resolution: "lie@npm:3.1.1"
dependencies:
immediate: "npm:~3.0.5"
checksum: c2c7d9dcc3a9aae641f41cde4e2e2cd571e4426b1f5915862781d77776672dcbca43461e16f4d382c9a300825c15e1a4923f1def3a5568d97577e077a3cecb44
languageName: node
linkType: hard
"lie@npm:~3.3.0":
version: 3.3.0
resolution: "lie@npm:3.3.0"
@@ -25356,6 +25455,15 @@ __metadata:
languageName: node
linkType: hard
"localforage@npm:^1.8.1":
version: 1.10.0
resolution: "localforage@npm:1.10.0"
dependencies:
lie: "npm:3.1.1"
checksum: d5c44be3a09169b013a3ebe252e678aaeb6938ffe72e9e12c199fd4307c1ec9d1a057ac2dfdfbb1379dfeec467a34ad0fc3ecd27489a2c43a154fb72b2822542
languageName: node
linkType: hard
"locate-path@npm:^2.0.0":
version: 2.0.0
resolution: "locate-path@npm:2.0.0"