From 14f17595aacadb2aca95ac0ad92bd48793e986ab Mon Sep 17 00:00:00 2001 From: Todd Schiller Date: Sun, 23 Apr 2023 10:48:15 -0400 Subject: [PATCH 1/5] #5530: add error boundary for IDB errors --- src/background/telemetry.ts | 77 ++++++++- src/components/ErrorBoundary.tsx | 122 +++++++++----- src/extensionConsole/App.tsx | 5 +- .../components/IDBErrorDisplay.tsx | 157 ++++++++++++++++++ .../pages/settings/StorageSettings.tsx | 55 +++--- src/telemetry/logging.ts | 2 +- src/types/browserTypes.ts | 47 ++++++ src/utils/idbUtils.ts | 36 ++++ 8 files changed, 418 insertions(+), 83 deletions(-) create mode 100644 src/extensionConsole/components/IDBErrorDisplay.tsx create mode 100644 src/types/browserTypes.ts diff --git a/src/background/telemetry.ts b/src/background/telemetry.ts index 37412c1d9..5dedfb7ad 100644 --- a/src/background/telemetry.ts +++ b/src/background/telemetry.ts @@ -27,9 +27,11 @@ import { maybeGetLinkedApiClient, } from "@/services/apiClient"; import { allowsTrack } from "@/telemetry/dnt"; -import { type DBSchema, openDB } from "idb/with-async-ittr"; +import { type DBSchema, type IDBPDatabase, openDB } from "idb/with-async-ittr"; import { type UnknownObject } from "@/types/objectTypes"; +import { deleteDatabase } from "@/utils/idbUtils"; +const UID_STORAGE_KEY = "USER_UUID" as ManualStorageKey; const EVENT_BUFFER_DEBOUNCE_MS = 2000; const EVENT_BUFFER_MAX_MS = 10_000; const TELEMETRY_DB_NAME = "telemetrydb"; @@ -50,15 +52,46 @@ interface TelemetryDB extends DBSchema { }; } +/** + * Singleton database connection. + */ +let databaseRef: IDBPDatabase | null = null; + async function openTelemetryDB() { - return openDB(TELEMETRY_DB_NAME, TELEMETRY_DB_VERSION_NUMBER, { - upgrade(db) { - // This is a new DB, so no need to delete existing object store yet - db.createObjectStore(TELEMETRY_EVENT_OBJECT_STORE, { - autoIncrement: true, - }); - }, + if (databaseRef) { + return databaseRef; + } + + databaseRef = await openDB( + TELEMETRY_DB_NAME, + TELEMETRY_DB_VERSION_NUMBER, + { + upgrade(db) { + // This is a new DB, so no need to delete existing object store yet + db.createObjectStore(TELEMETRY_EVENT_OBJECT_STORE, { + autoIncrement: true, + }); + }, + blocking() { + // Don't block closing/upgrading the database + console.debug("Closing telemetry database due to upgrade/delete"); + databaseRef?.close(); + databaseRef = null; + }, + terminated() { + console.debug( + "Telemetry database connection was unexpectedly terminated" + ); + databaseRef = null; + }, + } + ); + + databaseRef.addEventListener("close", () => { + databaseRef = null; }); + + return databaseRef; } async function addEvent(event: UserTelemetryEvent): Promise { @@ -74,7 +107,33 @@ export async function flushEvents(): Promise { return allEvents; } -const UID_STORAGE_KEY = "USER_UUID" as ManualStorageKey; +/** + * Deletes and recreates the logging database. + */ +export async function recreateDB(): Promise { + await deleteDatabase(TELEMETRY_DB_NAME); + + // Open the database to recreate it + await openTelemetryDB(); +} + +/** + * Returns the number of telemetry entries in the database. + */ +export async function count(): Promise { + const db = await openTelemetryDB(); + return db.count(TELEMETRY_EVENT_OBJECT_STORE); +} + +/** + * Clears all event entries from the database. + */ +export async function clear(): Promise { + const db = await openTelemetryDB(); + + const tx = db.transaction(TELEMETRY_EVENT_OBJECT_STORE, "readwrite"); + await tx.store.clear(); +} /** * Return a random ID for this browser profile. diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index fe363f8c3..673d083b4 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -23,29 +23,104 @@ import { getErrorMessage } from "@/errors/errorHelpers"; import { type UnknownObject } from "@/types/objectTypes"; import { isEmpty } from "lodash"; -interface Props { +interface DisplayProps { /** * Where the error happened, a hint in a free form */ errorContext?: string; } -interface State { +interface BoundaryProps extends DisplayProps { + /** + * Custom error display component + */ + ErrorComponent?: React.FC; +} + +interface ErrorState { + /** + * True if there was an error. Will always be true in ErrorDisplayProps + */ hasError: boolean; - errorMessage: string; - stack: string; + /** + * The error object + */ + error: unknown; + /** + * The error message, if available. + * @see getErrorMessage + */ + errorMessage: string | null; + /** + * The error stack trace, if available. + */ + stack: string | null; } -class ErrorBoundary extends Component { +/** + * Props passed to the ErrorComponent in the ErrorBoundary + * @see ErrorBoundary + */ +export type ErrorDisplayProps = DisplayProps & ErrorState; + +/** + * Default error display for use with ErrorBoundary + * @constructor + * @see ErrorBoundary + */ +export const DefaultErrorComponent: React.FC = ({ + errorContext, + errorMessage, + stack, +}) => ( +
+

Something went wrong.

+ {errorContext &&

{errorContext}

} + {!isEmpty(errorMessage) && ( +
+

{errorMessage}

+
+ )} +
+ +
+ {stack && ( +
+        {stack
+          // In the app
+          .replaceAll(location.origin + "/", "")
+          // In the content script
+          .replaceAll(
+            `chrome-extension://${process.env.CHROME_EXTENSION_ID}/`,
+            ""
+          )}
+      
+ )} +
+); + +class ErrorBoundary extends Component { constructor(props: UnknownObject) { super(props); - this.state = { hasError: false, errorMessage: undefined, stack: undefined }; + this.state = { + hasError: false, + error: undefined, + errorMessage: undefined, + stack: undefined, + }; } static getDerivedStateFromError(error: Error) { // Update state so the next render will show the fallback UI. return { hasError: true, + error, errorMessage: getErrorMessage(error), stack: error.stack, }; @@ -53,38 +128,9 @@ class ErrorBoundary extends Component { override render(): React.ReactNode { if (this.state.hasError) { - return ( -
-

Something went wrong.

- {this.props.errorContext &&

{this.props.errorContext}

} - {!isEmpty(this.state.errorMessage) && ( -
-

{this.state.errorMessage}

-
- )} -
- -
- {this.state.stack && ( -
-              {this.state.stack
-                // In the app
-                .replaceAll(location.origin + "/", "")
-                // In the content script
-                .replaceAll(
-                  `chrome-extension://${process.env.CHROME_EXTENSION_ID}/`,
-                  ""
-                )}
-            
- )} -
- ); + const ErrorComponent = this.props.ErrorComponent ?? DefaultErrorComponent; + + return ; } return this.props.children; diff --git a/src/extensionConsole/App.tsx b/src/extensionConsole/App.tsx index c85adee6d..ed5c8d114 100644 --- a/src/extensionConsole/App.tsx +++ b/src/extensionConsole/App.tsx @@ -53,6 +53,7 @@ import { logActions } from "@/components/logViewer/logSlice"; import ReduxPersistenceContext, { type ReduxPersistenceContextType, } from "@/store/ReduxPersistenceContext"; +import IDBErrorDisplay from "@/extensionConsole/components/IDBErrorDisplay"; // Register the built-in bricks registerEditors(); @@ -83,7 +84,7 @@ const Layout = () => { {/* It is guaranteed that under RequireAuth the user has a valid API token (either PixieBrix token or partner JWT). */} - + @@ -94,7 +95,7 @@ const Layout = () => {
- + . + */ + +import React from "react"; +import { + DefaultErrorComponent, + type ErrorDisplayProps, +} from "@/components/ErrorBoundary"; +import { isIDBConnectionError, isIDBQuotaError } from "@/utils/idbUtils"; +import useUserAction from "@/hooks/useUserAction"; +import { clearLogs, recreateDB as recreateLogDB } from "@/telemetry/logging"; +import { clearTraces, recreateDB as recreateTraceDB } from "@/telemetry/trace"; +import { recreateDB as recreateBrickDB } from "@/registry/localRegistry"; +// eslint-disable-next-line import/no-restricted-paths -- safe import because IDB is shared resource +import { + recreateDB as recreateEventDB, + clear as clearEvents, +} from "@/background/telemetry"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faRedo } from "@fortawesome/free-solid-svg-icons"; +import AsyncButton from "@/components/AsyncButton"; +import { sleep } from "@/utils"; +import { useAsyncState } from "@/hooks/common"; +import { type StorageEstimate } from "@/types/browserTypes"; +import { expectContext } from "@/utils/expectContext"; +import AsyncStateGate from "@/components/AsyncStateGate"; +import { round } from "lodash"; + +const ConnectionErrorDisplay: React.FC = () => { + const recoverAction = useUserAction( + async () => { + await Promise.all([ + recreateLogDB(), + recreateTraceDB(), + recreateBrickDB(), + recreateEventDB(), + ]); + }, + { + successMessage: "Recreated local databases. Reloading page.", + errorMessage: "Error recreating local databases", + }, + [] + ); + + return ( +
+

Something went wrong.

+
+

Error connecting to local database.

+
+
+ { + await recoverAction(); + // Put outside the action so user can see the success message before the page reloads. + await sleep(250); + location.reload(); + }} + > + Retry + +
+
+ ); +}; + +const QuotaErrorDisplay: React.FC = () => { + const state = useAsyncState( + async () => ({ + storageEstimate: (await navigator.storage.estimate()) as StorageEstimate, + }), + [] + ); + + const recoverAction = useUserAction( + async () => { + await Promise.all([clearLogs(), clearTraces(), clearEvents()]); + }, + { + successMessage: "Reclaimed local space. Reloading page.", + errorMessage: "Error reclaiming local space", + }, + [] + ); + + return ( +
+

Something went wrong.

+
+

Insufficient storage space available to PixieBrix.

+

}> + {({ data: { storageEstimate } }) => ( +

+ Using + {round(storageEstimate.usage / 1e6, 1).toLocaleString()} MB of + {round(storageEstimate.quota / 1e6, 0).toLocaleString()} MB + available +

+ )} +
+
+
+ { + await recoverAction(); + // Put outside the action so user can see the success message before the page reloads. + await sleep(250); + location.reload(); + }} + > + Reclaim Space + +
+
+ ); +}; + +/** + * A component that displays custom error messages for IDB errors. + * + * Use with ErrorBoundary.ErrorComponent + * + * @see ErrorBoundary + */ +const IDBErrorDisplay: React.FC = (props) => { + expectContext("extension"); + + const { error } = props; + + if (isIDBConnectionError(error)) { + return ; + } + + if (isIDBQuotaError(error)) { + return ; + } + + return ; +}; + +export default IDBErrorDisplay; diff --git a/src/extensionConsole/pages/settings/StorageSettings.tsx b/src/extensionConsole/pages/settings/StorageSettings.tsx index c806c6f0f..a993e1cc0 100644 --- a/src/extensionConsole/pages/settings/StorageSettings.tsx +++ b/src/extensionConsole/pages/settings/StorageSettings.tsx @@ -25,50 +25,27 @@ import { } from "@/registry/localRegistry"; import { clearLogs, - recreateDB as recreateLogDB, count as logSize, + recreateDB as recreateLogDB, } from "@/telemetry/logging"; +import { + clear as clearEvents, + count as eventsSize, + recreateDB as recreateEventDB, +} from "@/background/telemetry"; import AsyncButton from "@/components/AsyncButton"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBroom, faDatabase } from "@fortawesome/free-solid-svg-icons"; import useUserAction from "@/hooks/useUserAction"; import { clearTraces, - recreateDB as recreateTraceDB, count as traceSize, + recreateDB as recreateTraceDB, } from "@/telemetry/trace"; import AsyncStateGate, { StandardError } from "@/components/AsyncStateGate"; import cx from "classnames"; import styles from "@/extensionConsole/pages/settings/SettingsCard.module.scss"; - -/** - * https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/estimate - */ -type StorageEstimate = { - /** - * A numeric value in bytes approximating the amount of storage space currently being used by the site or Web app, - * out of the available space as indicated by quota. Unit is byte. - */ - usage: number; - /** - * A numeric value in bytes which provides a conservative approximation of the total storage the user's device or - * computer has available for the site origin or Web app. It's possible that there's more than this amount of space - * available though you can't rely on that being the case. - */ - quota: number; - - /** - * An object containing a breakdown of usage by storage system. All included properties will have a usage greater - * than 0 and any storage system with 0 usage will be excluded from the object. - */ - usageDetails: { - // https://github.com/whatwg/storage/issues/63#issuecomment-437990804 - indexedDB?: number; - caches?: number; - serviceWorkerRegistrations?: number; - other: number; - }; -}; +import { type StorageEstimate } from "@/types/browserTypes"; /** * React component to display local storage usage (to help identify storage problems) @@ -81,6 +58,7 @@ const StorageSettings: React.FunctionComponent = () => { brickCount: await registrySize(), logCount: await logSize(), traceCount: await traceSize(), + eventCount: await eventsSize(), }), [] ); @@ -89,7 +67,7 @@ const StorageSettings: React.FunctionComponent = () => { const clearLogsAction = useUserAction( async () => { - await Promise.all([clearLogs(), clearTraces()]); + await Promise.all([clearLogs(), clearTraces(), clearEvents()]); await recalculate(); }, { @@ -105,6 +83,7 @@ const StorageSettings: React.FunctionComponent = () => { recreateLogDB(), recreateTraceDB(), recreateBrickDB(), + recreateEventDB(), ]); await recalculate(); }, @@ -129,7 +108,13 @@ const StorageSettings: React.FunctionComponent = () => { renderError={(props) => } > {({ - data: { storageEstimate, brickCount, logCount, traceCount }, + data: { + storageEstimate, + brickCount, + logCount, + traceCount, + eventCount, + }, }) => ( @@ -160,6 +145,10 @@ const StorageSettings: React.FunctionComponent = () => { + + + +
# Trace Records {traceCount.toLocaleString()}
# Buffered Events{eventCount.toLocaleString()}
)} diff --git a/src/telemetry/logging.ts b/src/telemetry/logging.ts index c8019c19c..b9ef02aea 100644 --- a/src/telemetry/logging.ts +++ b/src/telemetry/logging.ts @@ -143,7 +143,7 @@ async function getDB() { databaseRef = null; }, terminated() { - console.debug("Brick database connection was unexpectedly terminated"); + console.debug("Log database connection was unexpectedly terminated"); databaseRef = null; }, }); diff --git a/src/types/browserTypes.ts b/src/types/browserTypes.ts new file mode 100644 index 000000000..d4227ed0e --- /dev/null +++ b/src/types/browserTypes.ts @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// XXX: the default types are not complete, so we need to augment them +/** + * https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/estimate + * @see navigator.storage.estimate + */ +export type StorageEstimate = { + /** + * A numeric value in bytes approximating the amount of storage space currently being used by the site or Web app, + * out of the available space as indicated by quota. Unit is byte. + */ + usage: number; + /** + * A numeric value in bytes which provides a conservative approximation of the total storage the user's device or + * computer has available for the site origin or Web app. It's possible that there's more than this amount of space + * available though you can't rely on that being the case. + */ + quota: number; + + /** + * An object containing a breakdown of usage by storage system. All included properties will have a usage greater + * than 0 and any storage system with 0 usage will be excluded from the object. + */ + usageDetails: { + // https://github.com/whatwg/storage/issues/63#issuecomment-437990804 + indexedDB?: number; + caches?: number; + serviceWorkerRegistrations?: number; + other: number; + }; +}; diff --git a/src/utils/idbUtils.ts b/src/utils/idbUtils.ts index 9a315d2b5..3c7df9b70 100644 --- a/src/utils/idbUtils.ts +++ b/src/utils/idbUtils.ts @@ -1,5 +1,18 @@ import pDefer from "p-defer"; import { deleteDB } from "idb/with-async-ittr"; +import { getErrorMessage } from "@/errors/errorHelpers"; + +// IDB Connection Error message strings: +// https://app.rollbar.com/a/pixiebrix/fix/item/pixiebrix/6675 +const connectionErrors = ["Error Opening IndexedDB"]; + +// IDB Quota Error message strings: +// https://app.rollbar.com/a/pixiebrix/fix/item/pixiebrix/7979 +// https://app.rollbar.com/a/pixiebrix/fix/item/pixiebrix/6681 +const quotaErrors = [ + "Encountered full disk while opening backing store for indexedDB.open", + "IndexedDB Full", +]; /** * Delete an IndexedDB database. @@ -17,3 +30,26 @@ export async function deleteDatabase(databaseName: string): Promise { }); await Promise.race([deletePromise, deferred.promise]); } + +/** + * Returns true if the error corresponds to not being able to connect to IndexedDB, e.g., due to a corrupt database. + * Does not include quota errors. + * @param error the error object + * @see isIDBQuotaError + */ +export function isIDBConnectionError(error: unknown): boolean { + const message = getErrorMessage(error); + return connectionErrors.some((connectionError) => + message.includes(connectionError) + ); +} + +/** + * Returns true if the error corresponds to IndexedDB not having enough available quota. + * @param error the error object + * @see isIDBConnectionError + */ +export function isIDBQuotaError(error: unknown): boolean { + const message = getErrorMessage(error); + return quotaErrors.some((quotaError) => message.includes(quotaError)); +} From 857af4e944f51c37b4bc616fc717220afdad0f53 Mon Sep 17 00:00:00 2001 From: Todd Schiller Date: Sun, 23 Apr 2023 10:53:37 -0400 Subject: [PATCH 2/5] #5530: record events for error tracking --- .../components/IDBErrorDisplay.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/extensionConsole/components/IDBErrorDisplay.tsx b/src/extensionConsole/components/IDBErrorDisplay.tsx index 13c75542f..1d1c91dc0 100644 --- a/src/extensionConsole/components/IDBErrorDisplay.tsx +++ b/src/extensionConsole/components/IDBErrorDisplay.tsx @@ -26,10 +26,10 @@ import useUserAction from "@/hooks/useUserAction"; import { clearLogs, recreateDB as recreateLogDB } from "@/telemetry/logging"; import { clearTraces, recreateDB as recreateTraceDB } from "@/telemetry/trace"; import { recreateDB as recreateBrickDB } from "@/registry/localRegistry"; -// eslint-disable-next-line import/no-restricted-paths -- safe import because IDB is shared resource import { recreateDB as recreateEventDB, clear as clearEvents, + // eslint-disable-next-line import/no-restricted-paths -- safe import because IDB is shared resource } from "@/background/telemetry"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faRedo } from "@fortawesome/free-solid-svg-icons"; @@ -40,8 +40,12 @@ import { type StorageEstimate } from "@/types/browserTypes"; import { expectContext } from "@/utils/expectContext"; import AsyncStateGate from "@/components/AsyncStateGate"; import { round } from "lodash"; +import { reportEvent } from "@/telemetry/events"; -const ConnectionErrorDisplay: React.FC = () => { +const ConnectionErrorDisplay: React.FC = ({ + errorMessage, + errorContext, +}) => { const recoverAction = useUserAction( async () => { await Promise.all([ @@ -68,6 +72,8 @@ const ConnectionErrorDisplay: React.FC = () => { { await recoverAction(); + // Must happen after the clear so the event doesn't get cleared from the event DB buffer + reportEvent("IDBRecoverConnection", { errorMessage, errorContext }); // Put outside the action so user can see the success message before the page reloads. await sleep(250); location.reload(); @@ -80,7 +86,10 @@ const ConnectionErrorDisplay: React.FC = () => { ); }; -const QuotaErrorDisplay: React.FC = () => { +const QuotaErrorDisplay: React.FC = ({ + errorMessage, + errorContext, +}) => { const state = useAsyncState( async () => ({ storageEstimate: (await navigator.storage.estimate()) as StorageEstimate, @@ -119,6 +128,13 @@ const QuotaErrorDisplay: React.FC = () => { { await recoverAction(); + // Must happen after the re-create so the event doesn't get cleared from the event DB buffer + reportEvent("IDBReclaimQuota", { + errorContext, + errorMessage, + usage: state[0]?.storageEstimate.usage, + quota: state[0]?.storageEstimate.quota, + }); // Put outside the action so user can see the success message before the page reloads. await sleep(250); location.reload(); From 318ff4d83f244959916d5172458ea7cff4679a03 Mon Sep 17 00:00:00 2001 From: Todd Schiller Date: Sun, 23 Apr 2023 11:00:20 -0400 Subject: [PATCH 3/5] #5530: add storybook entries --- src/__snapshots__/Storyshots.test.js.snap | 165 ++++++++++++++++++ .../components/IDBErrorDisplay.stories.tsx | 78 +++++++++ .../components/IDBErrorDisplay.tsx | 5 +- 3 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 src/extensionConsole/components/IDBErrorDisplay.stories.tsx diff --git a/src/__snapshots__/Storyshots.test.js.snap b/src/__snapshots__/Storyshots.test.js.snap index 2d637adc6..f28bbf72a 100644 --- a/src/__snapshots__/Storyshots.test.js.snap +++ b/src/__snapshots__/Storyshots.test.js.snap @@ -3724,6 +3724,171 @@ exports[`Storyshots Editor/LogToolbar Default 1`] = `
`; +exports[`Storyshots ExtensionConsole/IDBErrorDisplay Connection Error 1`] = ` +
+

+ Something went wrong. +

+
+

+ Error connecting to local database. +

+
+
+ +
+
+`; + +exports[`Storyshots ExtensionConsole/IDBErrorDisplay Normal Error 1`] = ` +
+

+ Something went wrong. +

+
+

+ This is a normal error +

+
+
+ +
+
+    Error: This is a normal error
+    at Object.<anonymous> (/Users/tschiller/projects/pixiebrix-extension/src/extensionConsole/components/IDBErrorDisplay.stories.tsx:52:21)
+    at Runtime._execModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1429:24)
+    at Runtime._loadModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1013:12)
+    at Runtime.requireModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:873:12)
+    at Runtime.requireModuleOrMock (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1039:21)
+    at requireContext (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/babel-plugin-require-context-hook/register.js:29:12)
+    at /Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:79:29
+    at Array.forEach (<anonymous>)
+    at /Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:77:18
+    at Array.forEach (<anonymous>)
+    at executeLoadable (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:76:10)
+    at executeLoadableForChanges (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:127:20)
+    at Object.getProjectAnnotations [as nextFn] (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/start.js:161:84)
+    at /Users/tschiller/projects/pixiebrix-extension/node_modules/synchronous-promise/index.js:217:29
+    at Array.forEach (<anonymous>)
+    at SynchronousPromise._runResolutions (/Users/tschiller/projects/pixiebrix-extension/node_modules/synchronous-promise/index.js:214:19)
+    at SynchronousPromise.then (/Users/tschiller/projects/pixiebrix-extension/node_modules/synchronous-promise/index.js:67:10)
+    at PreviewWeb.getProjectAnnotationsOrRenderError (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/preview-web/dist/cjs/Preview.js:159:63)
+    at PreviewWeb.initialize (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/preview-web/dist/cjs/Preview.js:138:19)
+    at Object.configure (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/start.js:187:17)
+    at Object.configure (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/react/dist/cjs/client/preview/index.js:35:24)
+    at Object.configure [as default] (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/addon-storyshots/dist/ts3.9/frameworks/configure.js:83:19)
+    at Object.load (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/addon-storyshots/dist/ts3.9/frameworks/react/loader.js:13:24)
+    at Object.loadFramework [as default] (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/addon-storyshots/dist/ts3.9/frameworks/frameworkLoader.js:26:19)
+    at testStorySnapshots (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/addon-storyshots/dist/ts3.9/api/index.js:28:94)
+    at Object.<anonymous> (/Users/tschiller/projects/pixiebrix-extension/src/Storyshots.test.js:29:15)
+    at Runtime._execModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1429:24)
+    at Runtime._loadModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1013:12)
+    at Runtime.requireModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:873:12)
+    at jestAdapter (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:77:13)
+    at runTestInternal (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runner/build/runTest.js:367:16)
+    at runTest (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runner/build/runTest.js:444:34)
+    at Object.worker (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runner/build/testWorker.js:106:12)
+  
+
+`; + +exports[`Storyshots ExtensionConsole/IDBErrorDisplay Quota Error 1`] = ` +
+

+ Something went wrong. +

+
+

+ Insufficient storage space available to PixieBrix. +

+

+

+
+ +
+
+`; + exports[`Storyshots Fields/ChildObjectField Primitive Value 1`] = `
. + */ + +/* + * Copyright (C) 2023 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from "react"; +import { type ComponentStory, type ComponentMeta } from "@storybook/react"; + +import IDBErrorDisplay from "@/extensionConsole/components/IDBErrorDisplay"; +import { getErrorMessage } from "@/errors/errorHelpers"; + +export default { + title: "ExtensionConsole/IDBErrorDisplay", + component: IDBErrorDisplay, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +const normalError = new Error("This is a normal error"); +const quotaError = new Error( + "Encountered full disk while opening backing store for indexedDB.open." +); +const connectionError = new Error("Error Opening IndexedDB"); + +export const NormalError = Template.bind({}); +NormalError.args = { + error: normalError, + errorMessage: getErrorMessage(normalError), + stack: normalError.stack, + hasError: true, +}; + +export const QuotaError = Template.bind({}); +QuotaError.args = { + error: quotaError, + errorMessage: getErrorMessage(quotaError), + stack: quotaError.stack, + hasError: true, +}; + +export const ConnectionError = Template.bind({}); +ConnectionError.args = { + error: connectionError, + errorMessage: getErrorMessage(connectionError), + stack: connectionError.stack, + hasError: true, +}; diff --git a/src/extensionConsole/components/IDBErrorDisplay.tsx b/src/extensionConsole/components/IDBErrorDisplay.tsx index 1d1c91dc0..04f8a1d36 100644 --- a/src/extensionConsole/components/IDBErrorDisplay.tsx +++ b/src/extensionConsole/components/IDBErrorDisplay.tsx @@ -116,9 +116,8 @@ const QuotaErrorDisplay: React.FC = ({

}> {({ data: { storageEstimate } }) => (

- Using - {round(storageEstimate.usage / 1e6, 1).toLocaleString()} MB of - {round(storageEstimate.quota / 1e6, 0).toLocaleString()} MB + Using {round(storageEstimate.usage / 1e6, 1).toLocaleString()} MB + of {round(storageEstimate.quota / 1e6, 0).toLocaleString()} MB available

)} From cccf2daf6c65f48146710fb3c7693e955cb22192 Mon Sep 17 00:00:00 2001 From: Todd Schiller Date: Sun, 23 Apr 2023 11:07:29 -0400 Subject: [PATCH 4/5] Handle error case for fetching quota --- src/extensionConsole/components/IDBErrorDisplay.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/extensionConsole/components/IDBErrorDisplay.tsx b/src/extensionConsole/components/IDBErrorDisplay.tsx index 04f8a1d36..84cb67f7c 100644 --- a/src/extensionConsole/components/IDBErrorDisplay.tsx +++ b/src/extensionConsole/components/IDBErrorDisplay.tsx @@ -113,7 +113,13 @@ const QuotaErrorDisplay: React.FC = ({

Something went wrong.

Insufficient storage space available to PixieBrix.

-

}> +

} + renderError={() => ( +

Unable to retrieve usage and quota.

+ )} + > {({ data: { storageEstimate } }) => (

Using {round(storageEstimate.usage / 1e6, 1).toLocaleString()} MB From 1c69fe634bc580d3f3e47eff5fc5fa1d001cd5eb Mon Sep 17 00:00:00 2001 From: Todd Schiller Date: Sun, 23 Apr 2023 11:29:31 -0400 Subject: [PATCH 5/5] #5530: fix flakey storyshot --- src/__snapshots__/Storyshots.test.js.snap | 38 ++----------------- .../components/IDBErrorDisplay.stories.tsx | 9 +++-- 2 files changed, 10 insertions(+), 37 deletions(-) diff --git a/src/__snapshots__/Storyshots.test.js.snap b/src/__snapshots__/Storyshots.test.js.snap index f28bbf72a..754dbda4c 100644 --- a/src/__snapshots__/Storyshots.test.js.snap +++ b/src/__snapshots__/Storyshots.test.js.snap @@ -3808,40 +3808,10 @@ exports[`Storyshots ExtensionConsole/IDBErrorDisplay Normal Error 1`] = `

-    Error: This is a normal error
-    at Object.<anonymous> (/Users/tschiller/projects/pixiebrix-extension/src/extensionConsole/components/IDBErrorDisplay.stories.tsx:52:21)
-    at Runtime._execModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1429:24)
-    at Runtime._loadModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1013:12)
-    at Runtime.requireModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:873:12)
-    at Runtime.requireModuleOrMock (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1039:21)
-    at requireContext (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/babel-plugin-require-context-hook/register.js:29:12)
-    at /Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:79:29
-    at Array.forEach (<anonymous>)
-    at /Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:77:18
-    at Array.forEach (<anonymous>)
-    at executeLoadable (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:76:10)
-    at executeLoadableForChanges (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/executeLoadable.js:127:20)
-    at Object.getProjectAnnotations [as nextFn] (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/start.js:161:84)
-    at /Users/tschiller/projects/pixiebrix-extension/node_modules/synchronous-promise/index.js:217:29
-    at Array.forEach (<anonymous>)
-    at SynchronousPromise._runResolutions (/Users/tschiller/projects/pixiebrix-extension/node_modules/synchronous-promise/index.js:214:19)
-    at SynchronousPromise.then (/Users/tschiller/projects/pixiebrix-extension/node_modules/synchronous-promise/index.js:67:10)
-    at PreviewWeb.getProjectAnnotationsOrRenderError (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/preview-web/dist/cjs/Preview.js:159:63)
-    at PreviewWeb.initialize (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/preview-web/dist/cjs/Preview.js:138:19)
-    at Object.configure (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/core-client/dist/cjs/preview/start.js:187:17)
-    at Object.configure (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/react/dist/cjs/client/preview/index.js:35:24)
-    at Object.configure [as default] (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/addon-storyshots/dist/ts3.9/frameworks/configure.js:83:19)
-    at Object.load (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/addon-storyshots/dist/ts3.9/frameworks/react/loader.js:13:24)
-    at Object.loadFramework [as default] (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/addon-storyshots/dist/ts3.9/frameworks/frameworkLoader.js:26:19)
-    at testStorySnapshots (/Users/tschiller/projects/pixiebrix-extension/node_modules/@storybook/addon-storyshots/dist/ts3.9/api/index.js:28:94)
-    at Object.<anonymous> (/Users/tschiller/projects/pixiebrix-extension/src/Storyshots.test.js:29:15)
-    at Runtime._execModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1429:24)
-    at Runtime._loadModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:1013:12)
-    at Runtime.requireModule (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runtime/build/index.js:873:12)
-    at jestAdapter (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapter.js:77:13)
-    at runTestInternal (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runner/build/runTest.js:367:16)
-    at runTest (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runner/build/runTest.js:444:34)
-    at Object.worker (/Users/tschiller/projects/pixiebrix-extension/node_modules/jest-runner/build/testWorker.js:106:12)
+    ContextError: Encountered full disk while opening backing store for indexedDB.open.
+at k (chrome-extension://mpjjildhmpddojocokjkgmlkkkfjnepo/bundles/85282.bundle.js:2:35318)
+ at $ (chrome-extension://mpjjildhmpddojocokjkgmlkkkfjnepo/bundles/85282.bundle.js:2:36577)
+ at async L.runExtension (chrome-extension://mpjjildhmpddojocokjkgmlkkkfjnepo/bundles/contentScriptCore.bundle.js:1:220194)
   
`; diff --git a/src/extensionConsole/components/IDBErrorDisplay.stories.tsx b/src/extensionConsole/components/IDBErrorDisplay.stories.tsx index 95fabce5c..4f2b8ae1a 100644 --- a/src/extensionConsole/components/IDBErrorDisplay.stories.tsx +++ b/src/extensionConsole/components/IDBErrorDisplay.stories.tsx @@ -52,12 +52,15 @@ const quotaError = new Error( "Encountered full disk while opening backing store for indexedDB.open." ); const connectionError = new Error("Error Opening IndexedDB"); +// Hard-code a stack because the stack includes the file path on local/CI builds, so Storyshots will fail +const stack = + "ContextError: Encountered full disk while opening backing store for indexedDB.open.\nat k (chrome-extension://mpjjildhmpddojocokjkgmlkkkfjnepo/bundles/85282.bundle.js:2:35318)\n at $ (chrome-extension://mpjjildhmpddojocokjkgmlkkkfjnepo/bundles/85282.bundle.js:2:36577)\n at async L.runExtension (chrome-extension://mpjjildhmpddojocokjkgmlkkkfjnepo/bundles/contentScriptCore.bundle.js:1:220194)"; export const NormalError = Template.bind({}); NormalError.args = { error: normalError, errorMessage: getErrorMessage(normalError), - stack: normalError.stack, + stack, hasError: true, }; @@ -65,7 +68,7 @@ export const QuotaError = Template.bind({}); QuotaError.args = { error: quotaError, errorMessage: getErrorMessage(quotaError), - stack: quotaError.stack, + stack, hasError: true, }; @@ -73,6 +76,6 @@ export const ConnectionError = Template.bind({}); ConnectionError.args = { error: connectionError, errorMessage: getErrorMessage(connectionError), - stack: connectionError.stack, + stack, hasError: true, };