From 6c0f6cea30267555da7da980f859f112661096a1 Mon Sep 17 00:00:00 2001 From: Jiaxin Peng Date: Mon, 27 Oct 2025 22:34:46 +0000 Subject: [PATCH] feat: enhance logging and error handling in upload commands and base class --- src/upload/Upload2Notion.ts | 79 +++++++++++- src/upload/common/UploadBase.ts | 114 +++++++++++++++++- src/upload/uploadCommand.ts | 205 ++++++++++++++++++++++++++------ 3 files changed, 353 insertions(+), 45 deletions(-) diff --git a/src/upload/Upload2Notion.ts b/src/upload/Upload2Notion.ts index 0a2c390..077563c 100644 --- a/src/upload/Upload2Notion.ts +++ b/src/upload/Upload2Notion.ts @@ -1,7 +1,7 @@ import { App, TFile } from "obsidian"; import { markdownToBlocks } from "@jxpeng98/martian"; import * as yamlFrontMatter from "yaml-front-matter"; -import { LIMITS, paragraph } from "@jxpeng98/martian/src/notion"; +import { LIMITS, paragraph } from "@jxpeng98/martian"; import MyPlugin from "src/main"; import { DatabaseDetails } from "../ui/settingTabs"; import { updateYamlInfo } from "./updateYaml"; @@ -89,10 +89,12 @@ export class Upload2Notion extends UploadBase { } async sync(request: SyncRequest): Promise { - console.log(`[Upload2Notion] Start sync`, { + const startedAt = Date.now(); + this.debugLog("Upload2Notion", "Sync invoked", { dataset: request.dataset, - file: request.nowFile.path, - database: this.dbDetails.databaseID, + filePath: request.nowFile.path, + databaseId: this.dbDetails.databaseID, + abName: this.dbDetails.abName, }); let response: NotionPageResponse; @@ -112,6 +114,12 @@ export class Upload2Notion extends UploadBase { } console.log(`[Upload2Notion] Notion response status`, response.response?.status); + this.debugLog("Upload2Notion", "Sync completed", { + status: response.response?.status ?? null, + durationMs: Date.now() - startedAt, + pageId: response.data?.id ?? null, + pageUrl: response.data?.url ?? null, + }); if (response.response && response.response.status === 200) { console.log(`[Upload2Notion] Sync success`, { @@ -142,6 +150,11 @@ export class Upload2Notion extends UploadBase { notionLimits: {truncate: false}, }); const notionId = this.getNotionId(request.app, request.nowFile); + this.debugLog("Upload2Notion", "General dataset payload prepared", { + blockCount: blocks.length, + notionId: notionId ?? null, + tagCount: request.tags?.length ?? 0, + }); return this.upsertGeneral({ title: request.title, @@ -163,6 +176,12 @@ export class Upload2Notion extends UploadBase { }); this.splitRichTextParagraphs(blocks); const notionId = this.getNotionId(request.app, request.nowFile); + this.debugLog("Upload2Notion", "Next dataset payload prepared", { + blockCount: blocks.length, + notionId: notionId ?? null, + hasEmoji: !!request.emoji, + tags: request.tags || [], + }); return this.upsertNext({ title: request.title, @@ -191,6 +210,11 @@ export class Upload2Notion extends UploadBase { notionLimits: {truncate: false}, }); const notionId = this.getNotionId(request.app, request.nowFile); + this.debugLog("Upload2Notion", "Custom dataset payload prepared", { + blockCount: blocks.length, + notionId: notionId ?? null, + customValueKeys: Object.keys(request.customValues || {}), + }); return this.upsertCustom({ cover: request.cover, @@ -203,17 +227,31 @@ export class Upload2Notion extends UploadBase { private buildBlocks(markdown: string, options: Record): any[] { const yamlContent: any = yamlFrontMatter.loadFront(markdown); const content = yamlContent.__content; - return markdownToBlocks(content, options); + const blocks = markdownToBlocks(content, options); + this.debugLog("Upload2Notion", "Converted markdown to blocks", { + blockCount: blocks.length, + firstBlockTypes: blocks.slice(0, 5).map((block: any) => block?.type), + options, + }); + return blocks; } private getNotionId(app: App, nowFile: TFile): string | undefined { const frontMatter = app.metadataCache.getFileCache(nowFile)?.frontmatter; if (!frontMatter) { + this.debugLog("Upload2Notion", "No frontmatter found when resolving Notion ID", { + filePath: nowFile.path, + }); return undefined; } const {abName} = this.dbDetails; const notionIDKey = `NotionID-${abName}`; const notionId = frontMatter[notionIDKey]; + this.debugLog("Upload2Notion", "Resolved Notion ID from frontmatter", { + filePath: nowFile.path, + notionId: notionId ? String(notionId) : null, + frontMatterKeys: Object.keys(frontMatter), + }); return notionId ? String(notionId) : undefined; } @@ -247,8 +285,20 @@ export class Upload2Notion extends UploadBase { const cover = params.notionId ? await this.resolveCoverForUpdate(params.cover) : params.cover; + this.debugLog("Upload2Notion", "Upserting general page", { + title: params.title, + blockCount: params.childArr.length, + firstChunkSize: firstChunk.length, + extraChunkCount: extraChunks.length, + cover: cover ?? null, + notionIdExisting: params.notionId ?? null, + tagList: params.tags, + }); if (params.notionId) { + console.log(`[Upload2Notion] Deleting existing Notion page`, { + notionId: params.notionId, + }); await this.deletePage(params.notionId); } @@ -267,6 +317,17 @@ export class Upload2Notion extends UploadBase { const cover = params.notionId ? await this.resolveCoverForUpdate(params.cover) : params.cover; + this.debugLog("Upload2Notion", "Upserting NotionNext page", { + title: params.title, + type: params.type, + slug: params.slug, + blockCount: params.childArr.length, + firstChunkSize: firstChunk.length, + extraChunkCount: extraChunks.length, + cover: cover ?? null, + hasEmoji: !!params.emoji, + notionIdExisting: params.notionId ?? null, + }); if (params.notionId) { await this.deletePage(params.notionId); @@ -296,6 +357,14 @@ export class Upload2Notion extends UploadBase { const cover = params.notionId ? await this.resolveCoverForUpdate(params.cover) : params.cover; + this.debugLog("Upload2Notion", "Upserting custom page", { + blockCount: params.childArr.length, + firstChunkSize: firstChunk.length, + extraChunkCount: extraChunks.length, + cover: cover ?? null, + notionIdExisting: params.notionId ?? null, + customValueKeys: Object.keys(params.customValues || {}), + }); if (params.notionId) { await this.deletePage(params.notionId); diff --git a/src/upload/common/UploadBase.ts b/src/upload/common/UploadBase.ts index 7b803c0..c59cfe1 100644 --- a/src/upload/common/UploadBase.ts +++ b/src/upload/common/UploadBase.ts @@ -24,7 +24,11 @@ export abstract class UploadBase { async deletePage(notionID: string) { const {notionAPI} = this.dbDetails; - return requestUrl({ + this.debugLog("UploadBase", "Deleting Notion page request issued", { + notionId: notionID, + apiTokenPreview: this.maskValue(notionAPI), + }); + return await requestUrl({ url: `https://api.notion.com/v1/blocks/${notionID}`, method: "DELETE", headers: { @@ -33,6 +37,15 @@ export abstract class UploadBase { "Notion-Version": "2022-06-28", }, body: "", + throw: false, + }).catch((error) => + this.handleRequestError(error, `Deleting Notion page ${notionID}`), + ).then((response) => { + this.debugLog("UploadBase", "Delete page response received", { + notionId: notionID, + status: response.status, + }); + return response; }); } @@ -43,6 +56,9 @@ export abstract class UploadBase { console.log(`Page includes ${childArrLength} blocks`); if (childArrLength <= 100) { + this.debugLog("UploadBase", "Blocks fit into a single request", { + totalBlocks: childArrLength, + }); return { firstChunk: childArr, extraChunks: [], @@ -54,6 +70,13 @@ export abstract class UploadBase { extraChunks.push(childArr.slice(i, i + 100)); } + this.debugLog("UploadBase", "Blocks split into multiple chunks", { + totalBlocks: childArrLength, + firstChunkSize: 100, + extraChunkCount: extraChunks.length, + lastChunkSize: extraChunks[extraChunks.length - 1].length, + }); + return { firstChunk: childArr.slice(0, 100), extraChunks, @@ -76,18 +99,37 @@ export abstract class UploadBase { }, }; } + + this.debugLog("UploadBase", "Cover applied to payload", { + chosenCover: body.cover?.external?.url ?? null, + defaultBannerUsed: !cover && !!this.plugin.settings.bannerUrl, + }); } protected async resolveCoverForUpdate(cover?: string): Promise { if (cover) { + this.debugLog("UploadBase", "Existing cover retained for update", { + cover, + }); return cover; } const databaseCover = await this.fetchDatabaseCover(); + this.debugLog("UploadBase", "Cover fetched from database", { + databaseCover: databaseCover ?? null, + }); return databaseCover ?? undefined; } protected async submitPage(body: any, extraChunks: any[][]): Promise { const {notionAPI} = this.dbDetails; + const startedAt = Date.now(); + + this.debugLog("UploadBase", "Submitting page creation request", { + apiTokenPreview: this.maskValue(notionAPI), + propertyKeys: Object.keys(body?.properties ?? {}), + childrenCount: Array.isArray(body?.children) ? body.children.length : 0, + extraChunkCount: extraChunks.length, + }); const response = await requestUrl({ url: `https://api.notion.com/v1/pages`, @@ -99,9 +141,17 @@ export abstract class UploadBase { }, body: JSON.stringify(body), throw: false, - }); + }).catch((error) => + this.handleRequestError(error, "Creating Notion page"), + ); const data = await response.json; + this.debugLog("UploadBase", "Page creation response received", { + status: response.status, + durationMs: Date.now() - startedAt, + notionUrl: data?.url ?? null, + pageId: data?.id ?? null, + }); if (response.status !== 200) { new Notice(`Error ${data.status}: ${data.code} \n ${i18nConfig["CheckConsole"]}`, 5000); @@ -138,7 +188,11 @@ export abstract class UploadBase { children: chunk, }; - console.log(extraBlocks); + this.debugLog("UploadBase", "Appending extra blocks chunk", { + chunkIndex: i, + chunkSize: chunk.length, + pageId, + }); const extraResponse = await requestUrl({ url: `https://api.notion.com/v1/blocks/${pageId}/children`, @@ -149,9 +203,18 @@ export abstract class UploadBase { "Notion-Version": "2022-06-28", }, body: JSON.stringify(extraBlocks), - }); + throw: false, + }).catch((error) => + this.handleRequestError(error, `Appending blocks to page ${pageId}`), + ); const extraData: any = await extraResponse.json; + this.debugLog("UploadBase", "Append blocks response received", { + chunkIndex: i, + status: extraResponse.status, + pageId, + hasError: extraResponse.status !== 200, + }); if (extraResponse.status !== 200) { new Notice(`Error ${extraData.status}: ${extraData.code} \n ${i18nConfig["CheckConsole"]}`, 5000); @@ -168,6 +231,10 @@ export abstract class UploadBase { private async fetchDatabaseCover(): Promise { const {notionAPI, databaseID} = this.dbDetails; + this.debugLog("UploadBase", "Fetching database cover", { + databaseId: databaseID, + apiTokenPreview: this.maskValue(notionAPI), + }); const response = await requestUrl({ url: `https://api.notion.com/v1/databases/${databaseID}`, method: "GET", @@ -175,6 +242,14 @@ export abstract class UploadBase { Authorization: "Bearer " + notionAPI, "Notion-Version": "2022-06-28", }, + throw: false, + }).catch((error) => + this.handleRequestError(error, `Fetching database ${databaseID}`), + ); + + this.debugLog("UploadBase", "Database cover response received", { + status: response.status, + hasCover: !!response.json.cover, }); if (response.json.cover && response.json.cover.external) { @@ -183,4 +258,35 @@ export abstract class UploadBase { return null; } + + protected debugLog(context: string, message: string, payload?: Record): void { + if (payload) { + console.log(`[${context}] ${message}`, payload); + } else { + console.log(`[${context}] ${message}`); + } + } + + protected maskValue(value?: string, visibleChars = 4): string | undefined { + if (!value) { + return value; + } + if (value.length <= visibleChars * 2) { + return `${value.slice(0, visibleChars)}***`; + } + return `${value.slice(0, visibleChars)}***${value.slice(-visibleChars)}`; + } + + private handleRequestError(error: unknown, context: string): never { + const message = error instanceof Error && error.message + ? error.message + : String(error); + console.error(`[UploadBase] ${context} failed`, { + message, + error, + }); + throw new Error( + `${context} failed: ${message}. Please check your network connection or proxy settings.`, + ); + } } diff --git a/src/upload/uploadCommand.ts b/src/upload/uploadCommand.ts index 879e0c6..5f3b3df 100644 --- a/src/upload/uploadCommand.ts +++ b/src/upload/uploadCommand.ts @@ -1,12 +1,39 @@ import {i18nConfig} from "../lang/I18n"; import {App, Notice} from "obsidian"; import {Upload2Notion} from "./Upload2Notion"; +import type {NotionPageResponse} from "./common/UploadBase"; import {DatabaseDetails, PluginSettings} from "../ui/settingTabs"; import ObsidianSyncNotionPlugin from "../main"; import {getNowFileMarkdownContentNext} from "./common/getMarkdownNext"; import {getNowFileMarkdownContentGeneral} from "./common/getMarkdownGeneral"; import {getNowFileMarkdownContentCustom} from "./common/getMarkdownCustom"; +const SYNC_ERROR_NOTICE_DURATION = 8000; + +function extractErrorMessage(error: unknown): string { + if (error instanceof Error && error.message) { + return error.message; + } + return String(error); +} + +function notifySyncError(prefix: string, basename: string, error: unknown): void { + const errorMessage = extractErrorMessage(error); + console.error(`${prefix} Sync failed`, error); + new Notice( + `${i18nConfig["sync-fail"]} ${basename}. ${errorMessage}\n${i18nConfig["CheckConsole"]}`, + SYNC_ERROR_NOTICE_DURATION, + ); +} + +function logCommandDebug(context: string, message: string, payload?: Record): void { + if (payload) { + console.log(`[${context}] ${message}`, payload); + } else { + console.log(`[${context}] ${message}`); + } +} + export async function uploadCommandNext( plugin: ObsidianSyncNotionPlugin, settings: PluginSettings, @@ -16,6 +43,10 @@ export async function uploadCommandNext( const {notionAPI, databaseID} = dbDetails; console.log(`[uploadCommandNext] ${new Date().toISOString()} Triggered for file`, app.workspace.getActiveFile()?.path); + logCommandDebug("uploadCommandNext", "Command invoked", { + databaseId: databaseID, + apiTokenPreview: `${notionAPI?.slice(0, 4) ?? ""}***`, + }); // Check if the user has set up the Notion API and database ID if (notionAPI === "" || databaseID === "") { @@ -38,38 +69,76 @@ export async function uploadCommandNext( paword, favicon, datetime - } = await getNowFileMarkdownContentNext(app, settings) + } = await getNowFileMarkdownContentNext(app, settings); + + logCommandDebug("uploadCommandNext", "Metadata extracted from markdown", { + hasMarkdown: !!markDownData, + markdownLength: markDownData?.length ?? 0, + tagCount: tags?.length ?? 0, + coverIncluded: !!cover, + hasEmoji: !!emoji, + type, + slug, + hasPassword: !!paword, + datetime, + }); if (markDownData) { const {basename} = nowFile; + logCommandDebug("uploadCommandNext", "Preparing to upload", { + filename: basename, + filePath: nowFile.path, + }); const upload = new Upload2Notion(plugin, dbDetails); - const res = await upload.sync({ - dataset: "next", - title: basename, - emoji: emoji || "", - cover: cover || "", - tags: tags || [], - type: type || "", - slug: slug || "", - stats: stats || "", - category: category || "", - summary: summary || "", - password: paword || "", - favicon: favicon || "", - datetime: datetime || "", - markdown: markDownData, - nowFile, - app, - }); + let res: NotionPageResponse; + try { + res = await upload.sync({ + dataset: "next", + title: basename, + emoji: emoji || "", + cover: cover || "", + tags: tags || [], + type: type || "", + slug: slug || "", + stats: stats || "", + category: category || "", + summary: summary || "", + password: paword || "", + favicon: favicon || "", + datetime: datetime || "", + markdown: markDownData, + nowFile, + app, + }); + } catch (error: unknown) { + notifySyncError("[uploadCommandNext]", basename, error); + logCommandDebug("uploadCommandNext", "Sync threw error", { + filename: basename, + error: extractErrorMessage(error), + }); + return; + } const {response} = res; if (response.status === 200) { new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green"; console.log(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`); + logCommandDebug("uploadCommandNext", "Sync succeeded", { + filename: basename, + status: response.status, + pageId: res.data?.id ?? null, + pageUrl: res.data?.url ?? null, + }); } else { new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000); console.log(`${i18nConfig["sync-fail"]} ${basename}`); + logCommandDebug("uploadCommandNext", "Sync failed with status", { + filename: basename, + status: response.status, + errorCode: res.data?.code ?? null, + errorStatus: res.data?.status ?? null, + }); } } @@ -85,6 +154,10 @@ export async function uploadCommandGeneral( const {notionAPI, databaseID} = dbDetails; console.log(`[uploadCommandGeneral] ${new Date().toISOString()} Triggered for file`, app.workspace.getActiveFile()?.path); + logCommandDebug("uploadCommandGeneral", "Command invoked", { + databaseId: databaseID, + apiTokenPreview: `${notionAPI?.slice(0, 4) ?? ""}***`, + }); // Check if the user has set up the Notion API and database ID if (notionAPI === "" || databaseID === "") { @@ -100,25 +173,53 @@ export async function uploadCommandGeneral( if (markDownData) { const {basename} = nowFile; + logCommandDebug("uploadCommandGeneral", "Preparing to upload", { + filename: basename, + markdownLength: markDownData.length, + tagCount: tags?.length ?? 0, + hasCover: !!cover, + }); const upload = new Upload2Notion(plugin, dbDetails); - const res = await upload.sync({ - dataset: "general", - title: basename, - cover: cover || "", - tags: tags || [], - markdown: markDownData, - nowFile, - app, - }); + let res: NotionPageResponse; + try { + res = await upload.sync({ + dataset: "general", + title: basename, + cover: cover || "", + tags: tags || [], + markdown: markDownData, + nowFile, + app, + }); + } catch (error: unknown) { + notifySyncError("[uploadCommandGeneral]", basename, error); + logCommandDebug("uploadCommandGeneral", "Sync threw error", { + filename: basename, + error: extractErrorMessage(error), + }); + return; + } const {response} = res; if (response.status === 200) { new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green"; console.log(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`); + logCommandDebug("uploadCommandGeneral", "Sync succeeded", { + filename: basename, + status: response.status, + pageId: res.data?.id ?? null, + pageUrl: res.data?.url ?? null, + }); } else { new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000); console.log(`${i18nConfig["sync-fail"]} ${basename}`); + logCommandDebug("uploadCommandGeneral", "Sync failed with status", { + filename: basename, + status: response.status, + errorCode: res.data?.code ?? null, + errorStatus: res.data?.status ?? null, + }); } } @@ -134,6 +235,10 @@ export async function uploadCommandCustom( const {notionAPI, databaseID} = dbDetails; console.log(`[uploadCommandCustom] ${new Date().toISOString()} Triggered for file`, app.workspace.getActiveFile()?.path); + logCommandDebug("uploadCommandCustom", "Command invoked", { + databaseId: databaseID, + apiTokenPreview: `${notionAPI?.slice(0, 4) ?? ""}***`, + }); // Check if the user has set up the Notion API and database ID if (notionAPI === "" || databaseID === "") { @@ -149,25 +254,53 @@ export async function uploadCommandCustom( if (markDownData) { const {basename} = nowFile; + logCommandDebug("uploadCommandCustom", "Preparing to upload", { + filename: basename, + markdownLength: markDownData.length, + hasCover: !!cover, + customValueKeys: Object.keys(customValues || {}), + }); const upload = new Upload2Notion(plugin, dbDetails); - const res = await upload.sync({ - dataset: "custom", - cover: cover || "", - customValues, - markdown: markDownData, - nowFile, - app, - }); + let res: NotionPageResponse; + try { + res = await upload.sync({ + dataset: "custom", + cover: cover || "", + customValues, + markdown: markDownData, + nowFile, + app, + }); + } catch (error: unknown) { + notifySyncError("[uploadCommandCustom]", basename, error); + logCommandDebug("uploadCommandCustom", "Sync threw error", { + filename: basename, + error: extractErrorMessage(error), + }); + return; + } const {response} = res; if (response.status === 200) { new Notice(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`).noticeEl.style.color = "green"; console.log(`${i18nConfig["sync-preffix"]} ${basename} ${i18nConfig["sync-success"]}`); + logCommandDebug("uploadCommandCustom", "Sync succeeded", { + filename: basename, + status: response.status, + pageId: res.data?.id ?? null, + pageUrl: res.data?.url ?? null, + }); } else { new Notice(`${i18nConfig["sync-fail"]} ${basename}`, 5000); console.log(`${i18nConfig["sync-fail"]} ${basename}`); + logCommandDebug("uploadCommandCustom", "Sync failed with status", { + filename: basename, + status: response.status, + errorCode: res.data?.code ?? null, + errorStatus: res.data?.status ?? null, + }); } }