From ae0fd1155f03971034cd484f783d16d59acb0170 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 12 Dec 2025 12:27:44 -0500 Subject: [PATCH 01/11] eng-1135 define internal-error --- .../src/components/CreateRelationDialog.tsx | 35 ++++----- apps/roam/src/components/Export.tsx | 28 +++---- apps/roam/src/components/canvas/Clipboard.tsx | 73 +++++++------------ apps/roam/src/components/canvas/Tldraw.tsx | 10 +-- .../src/components/canvas/useRoamStore.ts | 17 ++--- apps/roam/src/utils/internalError.ts | 55 ++++++++++++++ apps/roam/src/utils/syncDgNodesToSupabase.ts | 8 +- 7 files changed, 126 insertions(+), 100 deletions(-) create mode 100644 apps/roam/src/utils/internalError.ts diff --git a/apps/roam/src/components/CreateRelationDialog.tsx b/apps/roam/src/components/CreateRelationDialog.tsx index 83a8fd800..cf659769e 100644 --- a/apps/roam/src/components/CreateRelationDialog.tsx +++ b/apps/roam/src/components/CreateRelationDialog.tsx @@ -12,7 +12,6 @@ import { render as renderToast } from "roamjs-components/components/Toast"; import MenuItemSelect from "roamjs-components/components/MenuItemSelect"; import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; -import sendErrorEmail from "~/utils/sendErrorEmail"; import { getSetting } from "~/utils/extensionSettings"; import getDiscourseRelations, { type DiscourseRelation, @@ -21,7 +20,7 @@ import { createReifiedRelation } from "~/utils/createReifiedBlock"; import { findDiscourseNodeByTitleAndUid } from "~/utils/findDiscourseNode"; import { getDiscourseNodeFormatInnerExpression } from "~/utils/getDiscourseNodeFormatExpression"; import type { DiscourseNode } from "~/utils/getDiscourseNodes"; -import type { Result } from "~/utils/types"; +import internalError from "~/utils/internalError"; import getDiscourseNodes from "~/utils/getDiscourseNodes"; export type CreateRelationDialogProps = { @@ -39,15 +38,6 @@ type ExtendedCreateRelationDialogProps = CreateRelationDialogProps & { selectedSourceType: DiscourseNode; }; -const internalError = (msg: string) => { - process.env.NODE_ENV === "development" - ? console.error(msg) - : void sendErrorEmail({ - error: new Error(msg), - type: "Create Relation Dialog Failed", - }).catch(() => {}); -}; - const CreateRelationDialog = ({ onClose, sourceNodeUid, @@ -112,7 +102,11 @@ const CreateRelationDialog = ({ }); if (selectedTargetType === false) { // should not happen at this point, since the pattern was vetted at input. - internalError("Could not find identify node downstream"); + internalError({ + type: "create-relation-error", + error: + "Create Relation dialog: Could not find identify node downstream", + }); return null; } const candidateRelations = relDataByTag[selectedRelationName].filter( @@ -132,14 +126,18 @@ const CreateRelationDialog = ({ ); if (candidateRelations.length === 0) { // also should not happen - internalError("Could not find the relation"); + internalError({ + type: "create-relation-error", + error: "Create Relation dialog: Could not find the relation", + }); return null; } if (candidateRelations.length !== 1) { // This seems to happen... I need more data. - internalError( - `Too many relations between ${selectedTargetType.type} and ${selectedSourceType.type}: ${candidateRelations.map((r) => r.id).join(",")}`, - ); + internalError({ + type: "create-relation-error", + error: `Create Relation dialog: Too many relations between ${selectedTargetType.type} and ${selectedSourceType.type}: ${candidateRelations.map((r) => r.id).join(",")}`, + }); return null; } return candidateRelations[0]; @@ -288,7 +286,10 @@ const prepareRelData = ( }); if (!nodeSchema) { // should not happen at this point, since the pattern was vetted at input. - internalError("Could not find identify node downstream"); + internalError({ + error: "Create Relation dialog: Could not find identify node downstream", + type: "create-relation-error", + }); return []; } // note the same relation could be used in both directions diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index ee8756898..7294a64d3 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -54,7 +54,7 @@ import { import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg"; import { DiscourseNodeShape } from "~/components/canvas/DiscourseNodeUtil"; import { MAX_WIDTH } from "~/components/canvas/Tldraw"; -import sendErrorEmail from "~/utils/sendErrorEmail"; +import internalError from "~/utils/internalError"; import { getSetting, setSetting } from "~/utils/extensionSettings"; const ExportProgress = ({ id }: { id: string }) => { @@ -468,13 +468,12 @@ const ExportDialog: ExportDialogComponent = ({ id: "query-builder-export-success", }); } catch (e) { - const error = e as Error; - renderToast({ - content: "Looks like there was an error. The team has been notified.", - intent: "danger", - id: "discourse-graphs-error", + internalError({ + error: e as Error, + type: "export-error", + userMessage: + "Looks like there was an error. The team has been notified.", }); - sendErrorEmail({ error, type: "Export Dialog Failed" }).catch(() => {}); } finally { setLoading(false); onClose(); @@ -688,18 +687,13 @@ const ExportDialog: ExportDialogComponent = ({ setError(`Unsupported export type: ${exportType}`); } } catch (e) { - const error = e as Error; - renderToast({ - id: "export-error", - content: + internalError({ + error: e as Error, + type: "export-error", + userMessage: "Looks like there was an error. The team has been notified.", - intent: "danger", - }); - sendErrorEmail({ - error, - type: "Export Dialog Failed", context: { activeExportType, filename, results }, - }).catch(() => {}); + }); setDialogOpen(true); setError((e as Error).message); } finally { diff --git a/apps/roam/src/components/canvas/Clipboard.tsx b/apps/roam/src/components/canvas/Clipboard.tsx index 4ccab1881..34716d01e 100644 --- a/apps/roam/src/components/canvas/Clipboard.tsx +++ b/apps/roam/src/components/canvas/Clipboard.tsx @@ -57,7 +57,7 @@ import { MAX_WIDTH } from "./Tldraw"; import getBlockProps from "~/utils/getBlockProps"; import setBlockProps from "~/utils/setBlockProps"; import { measureCanvasNodeText } from "~/utils/measureCanvasNodeText"; -import sendErrorEmail from "~/utils/sendErrorEmail"; +import internalError from "~/utils/internalError"; export type ClipboardPage = { uid: string; @@ -79,21 +79,6 @@ const ClipboardContext = createContext(null); const CLIPBOARD_PROP_KEY = "pages"; -const toError = (error: unknown, fallbackMessage: string): Error => { - if (error instanceof Error) { - return error; - } - if (typeof error === "string") { - return new Error(error); - } - try { - const serialized = JSON.stringify(error); - return new Error(serialized || fallbackMessage); - } catch { - return new Error(fallbackMessage); - } -}; - const getOrCreateClipboardBlock = async ( canvasPageTitle: string, userUid: string, @@ -155,11 +140,13 @@ export const ClipboardProvider = ({ try { const userUid = getCurrentUserUid(); if (!userUid) { - sendErrorEmail({ - error: new Error("Missing current user UID"), - type: "Canvas Clipboard: Missing current user UID", - context: { canvasPageTitle }, - }).catch(() => {}); + internalError({ + error: new Error("Canvas Clipboard: Missing current user UID"), + type: "canvas-clipboard-missing-uid", + context: { + canvasPageTitle, + }, + }); setIsInitialized(true); return; } @@ -179,13 +166,12 @@ export const ClipboardProvider = ({ ) { setPages(storedPages as ClipboardPage[]); } - } catch (e) { - const normalizedError = toError(e, "Failed to initialize clipboard"); - sendErrorEmail({ - error: normalizedError, - type: "Canvas Clipboard: Failed to initialize", + } catch (error) { + internalError({ + error, + type: "canvas-clipboard-initialization-error", context: { canvasPageTitle }, - }).catch(() => {}); + }); } finally { setIsInitialized(true); } @@ -201,13 +187,12 @@ export const ClipboardProvider = ({ setBlockProps(clipboardBlockUid, { [CLIPBOARD_PROP_KEY]: pages, }); - } catch (e) { - const normalizedError = toError(e, "Failed to persist clipboard state"); - sendErrorEmail({ - error: normalizedError, - type: "Canvas Clipboard: Failed to persist state", + } catch (error) { + internalError({ + error, + type: "canvas-clipboard-state-save-error", context: { clipboardBlockUid, pageCount: pages.length }, - }).catch(() => {}); + }); } }, [pages, clipboardBlockUid, isInitialized]); @@ -287,8 +272,8 @@ const AddPageModal = ({ isOpen, onClose, onConfirm }: AddPageModalProps) => { // eslint-disable-next-line @typescript-eslint/await-thenable const raw = await window.roamAlphaAPI.data.backend.q( ` - [:find ?text ?uid - :where + [:find ?text ?uid + :where [?e :node/title ?text] [?e :block/uid ?uid]]`, ); @@ -447,15 +432,11 @@ const ClipboardPageSection = ({ ); setDiscourseNodes(nodes); } catch (error) { - const normalizedError = toError( + internalError({ error, - "Failed to fetch discourse nodes", - ); - sendErrorEmail({ - error: normalizedError, - type: "Canvas Clipboard: Failed to fetch discourse nodes", + type: "canvas-clipboard-discourse-node-error", context: { pageTitle: page.text }, - }).catch(() => {}); + }); setDiscourseNodes([]); } finally { setIsLoading(false); @@ -610,11 +591,11 @@ const ClipboardPageSection = ({ const nodeType = findDiscourseNode(node.uid); if (!nodeType) { - sendErrorEmail({ - error: new Error("Node type not found"), - type: "Canvas Clipboard: Node type not found", + internalError({ + error: new Error("Canvas Clipboard: Node type not found"), + type: "canvas-clipboard-type-not-found", context: { uid: node.uid }, - }).catch(() => {}); + }); return; } diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index 902a9723f..095a76dea 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -86,7 +86,7 @@ import { createMigrations } from "./DiscourseRelationShape/discourseRelationMigr import ToastListener, { dispatchToastEvent } from "./ToastListener"; import { CanvasDrawerPanel } from "./CanvasDrawer"; import { ClipboardPanel, ClipboardProvider } from "./Clipboard"; -import sendErrorEmail from "~/utils/sendErrorEmail"; +import internalError from "~/utils/internalError"; import { AUTO_CANVAS_RELATIONS_KEY } from "~/data/userSettings"; import { getSetting } from "~/utils/extensionSettings"; import { isPluginTimerReady, waitForPluginTimer } from "~/utils/pluginTimer"; @@ -516,16 +516,14 @@ const TldrawCanvas = ({ title }: { title: string }) => { error.stack = e.detail.stack; } - sendErrorEmail({ + internalError({ error, - type: "Tldraw Error", + type: "tldraw-error", context: { title: title, lastActions: lastActionsRef.current, }, - }).catch(() => {}); - - console.error("Tldraw Error:", e.detail); + }); }; document.addEventListener( diff --git a/apps/roam/src/components/canvas/useRoamStore.ts b/apps/roam/src/components/canvas/useRoamStore.ts index 95b9a9415..4d1825c85 100644 --- a/apps/roam/src/components/canvas/useRoamStore.ts +++ b/apps/roam/src/components/canvas/useRoamStore.ts @@ -25,8 +25,7 @@ import { } from "tldraw"; import { AddPullWatch } from "roamjs-components/types"; import { LEGACY_SCHEMA } from "~/data/legacyTldrawSchema"; -import sendErrorEmail from "~/utils/sendErrorEmail"; -import getCurrentUserDisplayName from "roamjs-components/queries/getCurrentUserDisplayName"; +import internalError from "~/utils/internalError"; const THROTTLE = 350; @@ -142,21 +141,20 @@ export const useRoamStore = ({ error: Error; errorMessage: string; }): void => { - console.error(errorMessage, error); setError(error); setLoading(false); const snapshotSize = initialSnapshot ? JSON.stringify(initialSnapshot).length : 0; - sendErrorEmail({ + internalError({ error, - type: errorMessage, + type: "roam-store-error", context: { pageUid, snapshotSize, ...(snapshotSize < 10000 ? { initialSnapshot } : {}), }, - }).catch(() => {}); + }); }; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -305,14 +303,13 @@ export const useRoamStore = ({ setNeedsUpgrade(false); setInitialSnapshot(null); setError(error); - sendErrorEmail({ + internalError({ error, - type: "Failed to perform Canvas upgrade", + type: "canvas-upgrade-error", context: { data: { oldData }, }, - }).catch(() => {}); - console.error("Failed to perform Canvas upgrade", error); + }); } }; diff --git a/apps/roam/src/utils/internalError.ts b/apps/roam/src/utils/internalError.ts new file mode 100644 index 000000000..91cd7c9cd --- /dev/null +++ b/apps/roam/src/utils/internalError.ts @@ -0,0 +1,55 @@ +import posthog from "posthog-js"; +import type { Properties } from "posthog-js"; +import renderToast from "roamjs-components/components/Toast"; +import { getVersionWithDate } from "~/utils/getVersion"; +import getCurrentUserDisplayName from "roamjs-components/queries/getCurrentUserDisplayName"; + +const internalError = ({ + error, + userMessage, + type, + context, +}: { + error: unknown; + type?: string; + userMessage?: string; + context?: Properties; +}) => { + if (process.env.NODE_ENV === "development") { + console.error(error, context); + } else { + const { version, buildDate } = getVersionWithDate(); + const username = getCurrentUserDisplayName(); + if (username) posthog.identify(username); + context = { + app: "Roam", + type: type || "internal-error", + graphName: window.roamAlphaAPI?.graph?.name || "unknown", + version: version || "-", + buildDate: buildDate || "-", + ...(context || {}), + }; + + if (typeof error === "string") { + error = new Error(error); + } else if (!(error instanceof Error)) { + try { + const serialized = JSON.stringify(error); + error = new Error(serialized); + } catch { + error = new Error(typeof error); + } + } + posthog.captureException(error, context); + } + if (userMessage !== undefined) { + renderToast({ + id: `${type || "internal-error"}-${Date.now()}`, + intent: "danger", + timeout: 5000, + content: userMessage, + }); + } +}; + +export default internalError; diff --git a/apps/roam/src/utils/syncDgNodesToSupabase.ts b/apps/roam/src/utils/syncDgNodesToSupabase.ts index 6f2013839..683fd9073 100644 --- a/apps/roam/src/utils/syncDgNodesToSupabase.ts +++ b/apps/roam/src/utils/syncDgNodesToSupabase.ts @@ -21,7 +21,7 @@ import { convertRoamNodeToLocalContent } from "./upsertNodesAsContentWithEmbeddi import { createClient, type DGSupabaseClient } from "@repo/database/lib/client"; import type { Json, CompositeTypes, Enums } from "@repo/database/dbTypes"; import { render as renderToast } from "roamjs-components/components/Toast"; -import sendErrorEmail from "~/utils/sendErrorEmail"; +import internalError from "~/utils/internalError"; type LocalContentDataInput = Partial>; type AccountLocalInput = CompositeTypes<"account_local_input">; @@ -60,11 +60,11 @@ const notifyEndSyncFailure = ({ }); } - sendErrorEmail({ + internalError({ error: new Error(reason), - type: "Sync Failed", + type: "sync-error", context: { status }, - }).catch(() => {}); + }); }; export const endSyncTask = async ( From 54e685d6ba50720ed1287e25afe0faf67cbf0657 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 12 Dec 2025 12:51:58 -0500 Subject: [PATCH 02/11] coderabbit comments --- apps/roam/src/components/CreateRelationDialog.tsx | 5 ++--- apps/roam/src/components/canvas/useRoamStore.ts | 10 +++++----- apps/roam/src/utils/internalError.ts | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/roam/src/components/CreateRelationDialog.tsx b/apps/roam/src/components/CreateRelationDialog.tsx index cf659769e..e0af7bc19 100644 --- a/apps/roam/src/components/CreateRelationDialog.tsx +++ b/apps/roam/src/components/CreateRelationDialog.tsx @@ -104,8 +104,7 @@ const CreateRelationDialog = ({ // should not happen at this point, since the pattern was vetted at input. internalError({ type: "create-relation-error", - error: - "Create Relation dialog: Could not find identify node downstream", + error: "Create Relation dialog: Could not identify node downstream", }); return null; } @@ -287,7 +286,7 @@ const prepareRelData = ( if (!nodeSchema) { // should not happen at this point, since the pattern was vetted at input. internalError({ - error: "Create Relation dialog: Could not find identify node downstream", + error: "Create Relation dialog: Could not identify node downstream", type: "create-relation-error", }); return []; diff --git a/apps/roam/src/components/canvas/useRoamStore.ts b/apps/roam/src/components/canvas/useRoamStore.ts index 4d1825c85..2ce0dd33c 100644 --- a/apps/roam/src/components/canvas/useRoamStore.ts +++ b/apps/roam/src/components/canvas/useRoamStore.ts @@ -136,10 +136,10 @@ export const useRoamStore = ({ const handleStoreError = ({ error, - errorMessage, + type, }: { error: Error; - errorMessage: string; + type: string; }): void => { setError(error); setLoading(false); @@ -148,7 +148,7 @@ export const useRoamStore = ({ : 0; internalError({ error, - type: "roam-store-error", + type, context: { pageUid, snapshotSize, @@ -169,7 +169,7 @@ export const useRoamStore = ({ } catch (e) { handleStoreError({ error: e as Error, - errorMessage: "Failed to create TLStore", + type: "tlstore-creation-failure", }); return null; } @@ -180,7 +180,7 @@ export const useRoamStore = ({ } catch (e) { handleStoreError({ error: e as Error, - errorMessage: "Failed to migrate snapshot", + type: "snapshot-migration-failure", }); return null; } diff --git a/apps/roam/src/utils/internalError.ts b/apps/roam/src/utils/internalError.ts index 91cd7c9cd..aca4f9a52 100644 --- a/apps/roam/src/utils/internalError.ts +++ b/apps/roam/src/utils/internalError.ts @@ -14,7 +14,7 @@ const internalError = ({ type?: string; userMessage?: string; context?: Properties; -}) => { +}): void => { if (process.env.NODE_ENV === "development") { console.error(error, context); } else { From 39a7989192229811454ea175430da91208769c4a Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 12 Dec 2025 19:42:59 -0500 Subject: [PATCH 03/11] coderabbit correction --- apps/roam/src/components/Export.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index 7294a64d3..a31f9b6da 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -692,7 +692,16 @@ const ExportDialog: ExportDialogComponent = ({ type: "export-error", userMessage: "Looks like there was an error. The team has been notified.", - context: { activeExportType, filename, results }, + context: { + activeExportType, + filename, + resultsCount: Array.isArray(results) + ? results.length + : undefined, + sampleUids: Array.isArray(results) + ? results.slice(0, 10).map((r) => r.uid) + : undefined, + }, }); setDialogOpen(true); setError((e as Error).message); From ed4a4d2be8489670e967c79a92051fd74a4e03df Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Sun, 14 Dec 2025 13:47:14 -0500 Subject: [PATCH 04/11] also sendMail along with posthog --- apps/roam/src/utils/internalError.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/roam/src/utils/internalError.ts b/apps/roam/src/utils/internalError.ts index aca4f9a52..f1e6525b7 100644 --- a/apps/roam/src/utils/internalError.ts +++ b/apps/roam/src/utils/internalError.ts @@ -3,17 +3,20 @@ import type { Properties } from "posthog-js"; import renderToast from "roamjs-components/components/Toast"; import { getVersionWithDate } from "~/utils/getVersion"; import getCurrentUserDisplayName from "roamjs-components/queries/getCurrentUserDisplayName"; +import sendErrorEmail from "~/utils/sendErrorEmail"; const internalError = ({ error, userMessage, type, context, + sendEmail, // true by default }: { error: unknown; type?: string; userMessage?: string; context?: Properties; + sendEmail?: boolean; }): void => { if (process.env.NODE_ENV === "development") { console.error(error, context); @@ -21,9 +24,10 @@ const internalError = ({ const { version, buildDate } = getVersionWithDate(); const username = getCurrentUserDisplayName(); if (username) posthog.identify(username); + type = type || "internal-error"; context = { app: "Roam", - type: type || "internal-error", + type, graphName: window.roamAlphaAPI?.graph?.name || "unknown", version: version || "-", buildDate: buildDate || "-", @@ -41,6 +45,16 @@ const internalError = ({ } } posthog.captureException(error, context); + if (sendEmail !== false) { + sendErrorEmail({ + // by now error is an Error but TS did not figure it out + error: error as Error, + type, + context, + }).catch(() => { + console.error("Could not send error email", error, type, context); + }); + } } if (userMessage !== undefined) { renderToast({ From bf2cb92621131458c8e62a24876f4b95335a1d6a Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Sun, 14 Dec 2025 18:50:42 -0500 Subject: [PATCH 05/11] use header-friendly types. slugify for posthog. --- .../src/components/CreateRelationDialog.tsx | 16 ++++++++-------- apps/roam/src/components/Export.tsx | 4 ++-- apps/roam/src/components/canvas/Clipboard.tsx | 12 ++++++------ apps/roam/src/components/canvas/Tldraw.tsx | 2 +- apps/roam/src/components/canvas/useRoamStore.ts | 6 +++--- apps/roam/src/utils/internalError.ts | 17 +++++++++++++---- apps/roam/src/utils/syncDgNodesToSupabase.ts | 2 +- 7 files changed, 34 insertions(+), 25 deletions(-) diff --git a/apps/roam/src/components/CreateRelationDialog.tsx b/apps/roam/src/components/CreateRelationDialog.tsx index e0af7bc19..b891426ed 100644 --- a/apps/roam/src/components/CreateRelationDialog.tsx +++ b/apps/roam/src/components/CreateRelationDialog.tsx @@ -103,8 +103,8 @@ const CreateRelationDialog = ({ if (selectedTargetType === false) { // should not happen at this point, since the pattern was vetted at input. internalError({ - type: "create-relation-error", - error: "Create Relation dialog: Could not identify node downstream", + type: "Create Relation dialog", + error: "Could not identify node downstream", }); return null; } @@ -126,16 +126,16 @@ const CreateRelationDialog = ({ if (candidateRelations.length === 0) { // also should not happen internalError({ - type: "create-relation-error", - error: "Create Relation dialog: Could not find the relation", + type: "Create Relation dialog", + error: "Could not find the relation", }); return null; } if (candidateRelations.length !== 1) { // This seems to happen... I need more data. internalError({ - type: "create-relation-error", - error: `Create Relation dialog: Too many relations between ${selectedTargetType.type} and ${selectedSourceType.type}: ${candidateRelations.map((r) => r.id).join(",")}`, + type: "Create Relation dialog", + error: `Too many relations between ${selectedTargetType.type} and ${selectedSourceType.type}: ${candidateRelations.map((r) => r.id).join(",")}`, }); return null; } @@ -286,8 +286,8 @@ const prepareRelData = ( if (!nodeSchema) { // should not happen at this point, since the pattern was vetted at input. internalError({ - error: "Create Relation dialog: Could not identify node downstream", - type: "create-relation-error", + error: "Could not identify node downstream", + type: "Create Relation dialog", }); return []; } diff --git a/apps/roam/src/components/Export.tsx b/apps/roam/src/components/Export.tsx index a31f9b6da..b2be2ddb1 100644 --- a/apps/roam/src/components/Export.tsx +++ b/apps/roam/src/components/Export.tsx @@ -470,7 +470,7 @@ const ExportDialog: ExportDialogComponent = ({ } catch (e) { internalError({ error: e as Error, - type: "export-error", + type: "Export Dialog Failed", userMessage: "Looks like there was an error. The team has been notified.", }); @@ -689,7 +689,7 @@ const ExportDialog: ExportDialogComponent = ({ } catch (e) { internalError({ error: e as Error, - type: "export-error", + type: "Export Dialog Failed", userMessage: "Looks like there was an error. The team has been notified.", context: { diff --git a/apps/roam/src/components/canvas/Clipboard.tsx b/apps/roam/src/components/canvas/Clipboard.tsx index 34716d01e..92b4ac050 100644 --- a/apps/roam/src/components/canvas/Clipboard.tsx +++ b/apps/roam/src/components/canvas/Clipboard.tsx @@ -141,8 +141,8 @@ export const ClipboardProvider = ({ const userUid = getCurrentUserUid(); if (!userUid) { internalError({ - error: new Error("Canvas Clipboard: Missing current user UID"), - type: "canvas-clipboard-missing-uid", + error: new Error("Missing current user UID"), + type: "Canvas Clipboard: Missing current user UID", context: { canvasPageTitle, }, @@ -169,7 +169,7 @@ export const ClipboardProvider = ({ } catch (error) { internalError({ error, - type: "canvas-clipboard-initialization-error", + type: "Canvas Clipboard: Failed to initialize", context: { canvasPageTitle }, }); } finally { @@ -190,7 +190,7 @@ export const ClipboardProvider = ({ } catch (error) { internalError({ error, - type: "canvas-clipboard-state-save-error", + type: "Canvas Clipboard: Failed to persist state", context: { clipboardBlockUid, pageCount: pages.length }, }); } @@ -434,7 +434,7 @@ const ClipboardPageSection = ({ } catch (error) { internalError({ error, - type: "canvas-clipboard-discourse-node-error", + type: "Canvas Clipboard: Failed to fetch discourse nodes", context: { pageTitle: page.text }, }); setDiscourseNodes([]); @@ -593,7 +593,7 @@ const ClipboardPageSection = ({ if (!nodeType) { internalError({ error: new Error("Canvas Clipboard: Node type not found"), - type: "canvas-clipboard-type-not-found", + type: "Canvas Clipboard: Node type not found", context: { uid: node.uid }, }); return; diff --git a/apps/roam/src/components/canvas/Tldraw.tsx b/apps/roam/src/components/canvas/Tldraw.tsx index 095a76dea..2bd9c1fe8 100644 --- a/apps/roam/src/components/canvas/Tldraw.tsx +++ b/apps/roam/src/components/canvas/Tldraw.tsx @@ -518,7 +518,7 @@ const TldrawCanvas = ({ title }: { title: string }) => { internalError({ error, - type: "tldraw-error", + type: "Tldraw Error", context: { title: title, lastActions: lastActionsRef.current, diff --git a/apps/roam/src/components/canvas/useRoamStore.ts b/apps/roam/src/components/canvas/useRoamStore.ts index 2ce0dd33c..803d55350 100644 --- a/apps/roam/src/components/canvas/useRoamStore.ts +++ b/apps/roam/src/components/canvas/useRoamStore.ts @@ -169,7 +169,7 @@ export const useRoamStore = ({ } catch (e) { handleStoreError({ error: e as Error, - type: "tlstore-creation-failure", + type: "Failed to create TLStore", }); return null; } @@ -180,7 +180,7 @@ export const useRoamStore = ({ } catch (e) { handleStoreError({ error: e as Error, - type: "snapshot-migration-failure", + type: "Failed to migrate snapshot", }); return null; } @@ -305,7 +305,7 @@ export const useRoamStore = ({ setError(error); internalError({ error, - type: "canvas-upgrade-error", + type: "Failed to perform Canvas upgrade", context: { data: { oldData }, }, diff --git a/apps/roam/src/utils/internalError.ts b/apps/roam/src/utils/internalError.ts index f1e6525b7..e336e53dc 100644 --- a/apps/roam/src/utils/internalError.ts +++ b/apps/roam/src/utils/internalError.ts @@ -5,6 +5,8 @@ import { getVersionWithDate } from "~/utils/getVersion"; import getCurrentUserDisplayName from "roamjs-components/queries/getCurrentUserDisplayName"; import sendErrorEmail from "~/utils/sendErrorEmail"; +const NON_WORD = /\W+/g; + const internalError = ({ error, userMessage, @@ -24,7 +26,8 @@ const internalError = ({ const { version, buildDate } = getVersionWithDate(); const username = getCurrentUserDisplayName(); if (username) posthog.identify(username); - type = type || "internal-error"; + type = type || "Internal Error"; + const slugType = type.replaceAll(NON_WORD, "-").toLowerCase(); context = { app: "Roam", type, @@ -44,15 +47,21 @@ const internalError = ({ error = new Error(typeof error); } } - posthog.captureException(error, context); + posthog.captureException(error, { ...context, type: slugType }); if (sendEmail !== false) { sendErrorEmail({ // by now error is an Error but TS did not figure it out error: error as Error, type, context, - }).catch(() => { - console.error("Could not send error email", error, type, context); + }).catch((sendError) => { + console.error( + "Could not send error email", + sendError, + error, + type, + context, + ); }); } } diff --git a/apps/roam/src/utils/syncDgNodesToSupabase.ts b/apps/roam/src/utils/syncDgNodesToSupabase.ts index 683fd9073..1a209d024 100644 --- a/apps/roam/src/utils/syncDgNodesToSupabase.ts +++ b/apps/roam/src/utils/syncDgNodesToSupabase.ts @@ -62,7 +62,7 @@ const notifyEndSyncFailure = ({ internalError({ error: new Error(reason), - type: "sync-error", + type: "Sync Failed", context: { status }, }); }; From 4fd3bbda0502acea45c71ca9910c553c42811a8e Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Sun, 14 Dec 2025 18:52:22 -0500 Subject: [PATCH 06/11] merge error --- apps/roam/src/components/CreateRelationDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/roam/src/components/CreateRelationDialog.tsx b/apps/roam/src/components/CreateRelationDialog.tsx index b891426ed..283eac863 100644 --- a/apps/roam/src/components/CreateRelationDialog.tsx +++ b/apps/roam/src/components/CreateRelationDialog.tsx @@ -20,6 +20,7 @@ import { createReifiedRelation } from "~/utils/createReifiedBlock"; import { findDiscourseNodeByTitleAndUid } from "~/utils/findDiscourseNode"; import { getDiscourseNodeFormatInnerExpression } from "~/utils/getDiscourseNodeFormatExpression"; import type { DiscourseNode } from "~/utils/getDiscourseNodes"; +import type { Result } from "~/utils/types"; import internalError from "~/utils/internalError"; import getDiscourseNodes from "~/utils/getDiscourseNodes"; From 248117188adf8313baba936d2a72b05e079c4966 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Sun, 14 Dec 2025 18:53:23 -0500 Subject: [PATCH 07/11] default param --- apps/roam/src/utils/internalError.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/roam/src/utils/internalError.ts b/apps/roam/src/utils/internalError.ts index e336e53dc..c90f46ade 100644 --- a/apps/roam/src/utils/internalError.ts +++ b/apps/roam/src/utils/internalError.ts @@ -12,7 +12,7 @@ const internalError = ({ userMessage, type, context, - sendEmail, // true by default + sendEmail = true, }: { error: unknown; type?: string; From d53b066f090a463717624c00ce09d6046da42d0e Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Mon, 15 Dec 2025 13:59:45 -0500 Subject: [PATCH 08/11] more info in posthog setup. Use internalError in admin panel. --- .../src/components/settings/AdminPanel.tsx | 5 ++-- apps/roam/src/index.ts | 24 ++++++++++++------- apps/roam/src/utils/internalError.ts | 13 +++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/roam/src/components/settings/AdminPanel.tsx b/apps/roam/src/components/settings/AdminPanel.tsx index 157805f58..5fa747694 100644 --- a/apps/roam/src/components/settings/AdminPanel.tsx +++ b/apps/roam/src/components/settings/AdminPanel.tsx @@ -449,10 +449,11 @@ const FeatureFlagsTab = (): React.ReactElement => { icon="send-message" onClick={() => { console.log("sending error email"); - sendErrorEmail({ + internalError({ error: new Error("test"), type: "Test", - }).catch(() => {}); + forceSendInDev: true, + }); }} > Send Error Email diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 420ff727c..dfe82b2ae 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -39,6 +39,7 @@ import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTit import { DISCOURSE_CONFIG_PAGE_TITLE } from "./utils/renderNodeConfigPage"; import { getSetting } from "./utils/extensionSettings"; import { STREAMLINE_STYLING_KEY } from "./data/userSettings"; +import { getVersionWithDate } from "~/utils/getVersion"; const initPostHog = () => { posthog.init("phc_SNMmBqwNfcEpNduQ41dBUjtGNEUEKAy6jTn63Fzsrax", { @@ -46,6 +47,20 @@ const initPostHog = () => { person_profiles: "identified_only", capture_pageview: false, autocapture: false, + loaded: (posthog) => { + const { version, buildDate } = getVersionWithDate(); + const userUid = getCurrentUserUid(); + const graphName = window.roamAlphaAPI.graph.name; + posthog.identify(userUid, { + graphName, + }); + posthog.register({ + version: version || "-", + buildDate: buildDate || "-", + graphName, + }); + posthog.capture("Extension Loaded"); + }, property_denylist: [ "$ip", // Still seeing ip in the event "$device_id", @@ -71,15 +86,6 @@ export default runExtension(async (onloadArgs) => { const isOffline = window.roamAlphaAPI.graph.type === "offline"; if (!isEncrypted && !isOffline) { initPostHog(); - const userUid = getCurrentUserUid(); - const graphName = window.roamAlphaAPI.graph.name; - posthog.identify(userUid, { - graphName, - }); - posthog.capture("Extension Loaded", { - graphName, - userUid, - }); } initFeedbackWidget(); diff --git a/apps/roam/src/utils/internalError.ts b/apps/roam/src/utils/internalError.ts index c90f46ade..f74216d5b 100644 --- a/apps/roam/src/utils/internalError.ts +++ b/apps/roam/src/utils/internalError.ts @@ -1,8 +1,6 @@ import posthog from "posthog-js"; import type { Properties } from "posthog-js"; import renderToast from "roamjs-components/components/Toast"; -import { getVersionWithDate } from "~/utils/getVersion"; -import getCurrentUserDisplayName from "roamjs-components/queries/getCurrentUserDisplayName"; import sendErrorEmail from "~/utils/sendErrorEmail"; const NON_WORD = /\W+/g; @@ -13,27 +11,22 @@ const internalError = ({ type, context, sendEmail = true, + forceSendInDev = false, }: { error: unknown; type?: string; userMessage?: string; context?: Properties; sendEmail?: boolean; + forceSendInDev?: boolean; }): void => { - if (process.env.NODE_ENV === "development") { + if (process.env.NODE_ENV === "development" && forceSendInDev !== true) { console.error(error, context); } else { - const { version, buildDate } = getVersionWithDate(); - const username = getCurrentUserDisplayName(); - if (username) posthog.identify(username); type = type || "Internal Error"; const slugType = type.replaceAll(NON_WORD, "-").toLowerCase(); context = { - app: "Roam", type, - graphName: window.roamAlphaAPI?.graph?.name || "unknown", - version: version || "-", - buildDate: buildDate || "-", ...(context || {}), }; From c95397ed91e6585ebeb0a11e26008692a58b1a94 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Mon, 15 Dec 2025 14:04:26 -0500 Subject: [PATCH 09/11] make sendErrorEmail explicit in admin panel, so we can change the default later --- apps/roam/src/components/settings/AdminPanel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/roam/src/components/settings/AdminPanel.tsx b/apps/roam/src/components/settings/AdminPanel.tsx index 5fa747694..22ab32b53 100644 --- a/apps/roam/src/components/settings/AdminPanel.tsx +++ b/apps/roam/src/components/settings/AdminPanel.tsx @@ -452,6 +452,7 @@ const FeatureFlagsTab = (): React.ReactElement => { internalError({ error: new Error("test"), type: "Test", + sendErrorEmail: true, forceSendInDev: true, }); }} From f2d47db07968b93e75dea1c1568b6b3f8b162c24 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Mon, 15 Dec 2025 14:09:05 -0500 Subject: [PATCH 10/11] forgot to change import --- apps/roam/src/components/settings/AdminPanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/roam/src/components/settings/AdminPanel.tsx b/apps/roam/src/components/settings/AdminPanel.tsx index 22ab32b53..b877ec810 100644 --- a/apps/roam/src/components/settings/AdminPanel.tsx +++ b/apps/roam/src/components/settings/AdminPanel.tsx @@ -30,7 +30,7 @@ import { import migrateRelations from "~/utils/migrateRelations"; import { countReifiedRelations } from "~/utils/createReifiedBlock"; import { DGSupabaseClient } from "@repo/database/lib/client"; -import sendErrorEmail from "~/utils/sendErrorEmail"; +import internalError from "~/utils/internalError"; import SuggestiveModeSettings from "./SuggestiveModeSettings"; import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; import refreshConfigTree from "~/utils/refreshConfigTree"; @@ -452,7 +452,7 @@ const FeatureFlagsTab = (): React.ReactElement => { internalError({ error: new Error("test"), type: "Test", - sendErrorEmail: true, + sendEmail: true, forceSendInDev: true, }); }} From 5505b711c877c18878b59c02b7a475a65ca0f60e Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Mon, 15 Dec 2025 14:13:36 -0500 Subject: [PATCH 11/11] forgot one instance of sendErrorEmail --- .../DiscourseRelationShape/DiscourseRelationUtil.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx index 198e75916..1c6915071 100644 --- a/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseRelationShape/DiscourseRelationUtil.tsx @@ -80,7 +80,7 @@ import getCurrentPageUid from "roamjs-components/dom/getCurrentPageUid"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import { AddReferencedNodeType } from "./DiscourseRelationTool"; import { dispatchToastEvent } from "~/components/canvas/ToastListener"; -import sendErrorEmail from "~/utils/sendErrorEmail"; +import internalError from "~/utils/internalError"; const COLOR_ARRAY = Array.from(textShapeProps.color.values) .filter((c) => !["red", "green", "grey"].includes(c)) @@ -564,12 +564,10 @@ export const createAllRelationShapeUtils = ( relationBlockUid: relation.id, }); else { - void sendErrorEmail({ - error: new Error( - "attempt to create a relation between non discourse nodes", - ), - type: "canvas-create-relation-non-dgn", - }).catch(() => undefined); + void internalError({ + error: "attempt to create a relation between non discourse nodes", + type: "Canvas create relation", + }); } } else { const { triples, label: relationLabel } = relation;