Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server: Resolves #9355: Add sync debug page to Joplin Server #9368

Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
97e3a51
Draft: Create debug report route on joplin server
personalizedrefrigerator Nov 21, 2023
d084136
Switch to seperate sync debug page
personalizedrefrigerator Nov 25, 2023
77cef1b
Move new JS to separate .js file
personalizedrefrigerator Nov 25, 2023
7ca0ab2
Update labels
personalizedrefrigerator Nov 25, 2023
f4dcbbf
Disable submit button while processing
personalizedrefrigerator Nov 25, 2023
f864283
Merge branch 'dev' into pr/joplin-server-debug-report
personalizedrefrigerator Nov 25, 2023
21ea1cb
Set up TypeScript building for the public/js directory
personalizedrefrigerator Nov 28, 2023
e73d8c1
Fix .gitignore
personalizedrefrigerator Nov 28, 2023
d127495
Simplify tsconfig.json
personalizedrefrigerator Nov 28, 2023
fdbe833
Remove debug logging
personalizedrefrigerator Nov 28, 2023
7cc313b
Fix indentation
personalizedrefrigerator Nov 28, 2023
bff383b
eslintignore: Ignore generated .d.ts files in server/public/js
personalizedrefrigerator Nov 28, 2023
0cf24fe
Compile TypeScript using Gulp
personalizedrefrigerator Nov 28, 2023
1c26a35
Fix build
personalizedrefrigerator Nov 28, 2023
a0ab036
Adjust CSS naming to be closer to RCSS
personalizedrefrigerator Nov 29, 2023
b10f79f
Merge remote-tracking branch 'upstream/dev' into pr/joplin-server-deb…
personalizedrefrigerator Dec 2, 2023
48c5e02
Move tsc to package.json
personalizedrefrigerator Dec 2, 2023
aa2dd7d
Additional documentation
personalizedrefrigerator Dec 5, 2023
117db82
Simplify additional documentation
personalizedrefrigerator Dec 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,7 @@ packages/renderer/index.js
packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/server/public/js/index/sync_debug.js
packages/tools/build-release-stats.test.js
packages/tools/build-release-stats.js
packages/tools/build-translation.js
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,7 @@ packages/renderer/index.js
packages/renderer/noteStyle.js
packages/renderer/pathUtils.js
packages/renderer/utils.js
packages/server/public/js/index/sync_debug.js
packages/tools/build-release-stats.test.js
packages/tools/build-release-stats.js
packages/tools/build-translation.js
Expand Down
26 changes: 26 additions & 0 deletions packages/server/gulpfile.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
const gulp = require('gulp');
const utils = require('@joplin/tools/gulp/utils');
const compilePackageInfo = require('@joplin/tools/compilePackageInfo');
const { execCommand } = require('@joplin/utils');
const fs = require('fs-extra');
const path = require('path');

const distDir = `${__dirname}/dist`;

const tsConfigPaths = [
path.join(__dirname, 'tsconfig.json'),
path.join(__dirname, 'public', 'js', 'tsconfig.json'),
];

const tasks = {
tsc: {
fn: async () => {
for (const tsConfigPath of tsConfigPaths) {
await execCommand(['tsc', '--project', tsConfigPath]);
}
},
},

watch: {
fn: async () => {
const watchTasks = tsConfigPaths.map(tsConfigPath => {
return execCommand(
['tsc', '--watch', '--preserveWatchOutput', '--project', tsConfigPath],
);
});
await Promise.all(watchTasks);
},
},

compilePackageInfo: {
fn: async () => {
await fs.mkdirp(distDir);
Expand Down
4 changes: 2 additions & 2 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
"devDropDb": "node dist/app.js --env dev --drop-db",
"start": "node dist/app.js",
"generateTypes": "rm -f db-buildTypes.sqlite && yarn run start --env buildTypes migrate latest && node dist/tools/generateTypes.js && mv db-buildTypes.sqlite schema.sqlite",
"tsc": "tsc --project tsconfig.json",
"tsc": "gulp tsc",
"test": "jest --verbose=false",
"test-ci": "yarn test",
"test-debug": "node --inspect node_modules/.bin/jest -- --verbose=false",
"clean": "gulp clean",
"populateDatabase": "JOPLIN_TESTS_SERVER_DB=pg node dist/utils/testing/populateDatabase",
"stripeListen": "stripe listen --forward-to http://joplincloud.local:22300/stripe/webhook",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json"
"watch": "gulp watch"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of moving this to gulp because that's yet another level of indirection. And we used to use gulp for watching and it caused various issues so I'd prefer if we stay as close as possible to the tsc binary for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some options:

  • Do compilation with TypeScript project references, as was done in an earlier commit.
    • Benefits:
      • Calling tsc for watching/building only requires one call (e.g. tsc --build --watch --preserveWatchOutput or tsc --build).
    • Drawbacks:
      • Requires building .d.ts files in the referenced project.
      • There doesn't seem to be a way to specify the root tsconfig.json file (other than by changing the current working directory). As the server package currently specifies a tsconfig.json for build/watch commands, I suspect that specifying the directory with the root tsconfig.json is (or was) important.
  • Current method — call tsc with execCommand from gulp (or similar method: Call tsc from a compile-typescript.ts file run by ts-node).
    • Benefits:
      • Can watch both projects at the same time
      • Runs tsc for both projects during the tsc step (rather than during build)
      • Works on non-bash-like shells (as opposed to tsc --watch --project project1 & ; tsc --watch --project project2)
    • Drawbacks:
      • Requires modifying @joplin/utils to make execCommand available during the build step or modifying @joplin/utils to also export TypeScript files and moving to a TypeScript gulp configuration file. (Alternatively, re-implementing execCommand.)
  • Create a separate watch-public command for the public/ directory and compile both with tsc --project ./tsconfig.jsoon && tsc --project ./public/tsconfig.json.
    • Benefits:
      • Simple — doesn't require running tsc from JavaScript or significantly changing build flags.
    • Drawbacks:
      • yarn run watch won't watch the public directory.
  • Use webpack or rollup and a build-bundled-js command that runs during yarn build.
    • Benefits:
      • Allows import statements in TypeScript that will be built.
      • Similar to how JavaScript for WebViews, etc. are bundled in other packages (e.g. app-mobile/)
    • Drawbacks:
      • More complicated to set up
      • Easier to unintentionally bundle large amounts of code

},
"dependencies": {
"@aws-sdk/client-s3": "3.296.0",
Expand Down
37 changes: 37 additions & 0 deletions packages/server/public/css/index/sync_debug.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@


personalizedrefrigerator marked this conversation as resolved.
Show resolved Hide resolved
#debug-tools-container {
--border-color: #444;
}

#debug-tools-container legend {
font-weight: bold;
}

#debug-tools-container fieldset {
border-left: 1px solid var(--border-color);
padding-left: 5px;
margin-top: 14px;
/* Required for proper overflow. See
https://stackoverflow.com/a/29499408 */
min-width: 0;
}

#debug-tools-container .output {
border: 1px solid var(--border-color);
border-radius: 5px;
margin: 5px;
padding: 5px;
}

#debug-tools-container .output pre {
white-space: pre-wrap;
word-wrap: break-word;
max-height: 500px;
overflow-y: auto;
border-radius: 5px;
}

#debug-tools-container .output.empty {
display: none;
}
157 changes: 157 additions & 0 deletions packages/server/public/js/index/sync_debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@

const itemLinkToId = (link: string) => {
// Examples of supported links:
// - joplin://x-callback-url/openFolder?id=6c8caeec01a34c0f95487a04ebb79cb9
// - joplin://x-callback-url/openNote?id=6c8caeec01a34c0f95487a04ebb79cb9
// - :/6c8caeec01a34c0f95487a04ebb79cb9
// - /home/user/.config/joplin-desktop/resources/6c8caeec01a34c0f95487a04ebb79cb9.svg
// - [title](:/6c8caeec01a34c0f95487a04ebb79cb9)
const linkRegexs = [
// External item
/^joplin:\/\/x-callback-url\/(?:openFolder|openNote)\?id=(\w+)$/,

// Internal links
/^\/:(\w+)$/,
/^!?\[.*\]\(:\/(\w+)\)$/,

// Resource file URLs
/^(?:file:\/\/)?.*[/\\]resources[/\\](\w+)\.\w+$/,
];

for (const regex of linkRegexs) {
const match = regex.exec(link);
if (match) {
return match[1];
}
}

return link;
};

type OnItemCheckerSubmitCallback = (
itemId: string, outputHeading: HTMLElement, outputDetails: HTMLElement,
)=> Promise<void>;

const setUpItemChecker = (parent: HTMLElement, onSubmit: OnItemCheckerSubmitCallback) => {
const button = document.createElement('button');
button.innerText = 'Submit';

const input = parent.querySelector('input');
const outputContainer = document.createElement('div');
outputContainer.classList.add('output', 'empty');

const outputHeading = document.createElement('h3');
const outputDetailsContainer = document.createElement('details');
const outputDetailsContent = document.createElement('pre');

outputHeading.setAttribute('aria-live', 'polite');

outputDetailsContainer.appendChild(outputDetailsContent);
outputContainer.replaceChildren(outputHeading, outputDetailsContainer);

button.onclick = async () => {
outputHeading.innerText = '⏳ Loading...';
outputDetailsContent.innerText = '';
outputContainer.classList.remove('error');
outputContainer.classList.remove('empty');
outputContainer.classList.add('loading');
button.disabled = true;

try {
await onSubmit(itemLinkToId(input.value), outputHeading, outputDetailsContent);
outputContainer.classList.remove('loading');
} catch (error) {
outputHeading.innerText = `⚠️ Error: ${error}`;
outputContainer.classList.add('error');
}

button.disabled = false;
};

parent.appendChild(button);
parent.appendChild(outputContainer);
};

const checkForItemOnServer = async (
itemId: string, outputHeadingElement: HTMLElement, outputDetailsElement: HTMLElement,
) => {
const fetchResult = await fetch(`/api/items/root:/${encodeURIComponent(itemId)}.md:/`);

if (fetchResult.ok) {
const result = await fetchResult.text();
outputHeadingElement.innerText = 'Item found!';
outputDetailsElement.innerText = result;
} else {
outputHeadingElement.innerText = `Item ${itemId}: ${fetchResult.statusText}`;
outputDetailsElement.innerText = '';
}
};

const checkForItemInInitialDiff = async (
itemId: string, outputHeadingElement: HTMLElement, outputDetailsElement: HTMLElement,
) => {
let cursor: string|undefined = undefined;

const waitForTimeout = (timeout: number) => {
return new Promise<void>(resolve => {
setTimeout(() => resolve(), timeout);
});
};

const readDiff = async function*() {
let hasMore = true;
let page = 1;
while (hasMore) {
const fetchResult = await fetch(
`/api/items/root/delta${
cursor ? `?cursor=${encodeURIComponent(cursor)}` : ''
}`,
);
if (!fetchResult.ok) {
throw new Error(`Error fetching items: ${fetchResult.statusText}`);
}

const json = await fetchResult.json();
hasMore = json.has_more;
cursor = json.cursor;

for (const item of json.items) {
yield item;
}

outputHeadingElement.innerText = `Processing page ${page++}...`;

// Avoid sending requests too frequently
await waitForTimeout(200); // ms
}
};

const allItems = [];
const matches = [];
let stoppedEarly = false;
for await (const item of readDiff()) {
// Include console logging to provide more information if readDiff() fails.
// eslint-disable-next-line no-console
console.log('Checking item', item);

if (item.item_name === `${itemId}.md`) {
matches.push(item);
stoppedEarly = true;
}
allItems.push(item);
}

outputHeadingElement.innerText
= matches.length > 0 ? 'Found in initial sync diff' : `Item ${itemId}: Not in initial sync diff`;

const stoppedEarlyDescription = (
stoppedEarly ? '\n Stopped fetching items after finding a match. Item list is incomplete.' : ''
);
outputDetailsElement.innerText
= JSON.stringify(allItems, undefined, ' ') + stoppedEarlyDescription;
};

document.addEventListener('DOMContentLoaded', () => {
setUpItemChecker(document.querySelector('#note-on-server-check'), checkForItemOnServer);
setUpItemChecker(document.querySelector('#note-in-diff-check'), checkForItemInInitialDiff);
});
15 changes: 15 additions & 0 deletions packages/server/public/js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
// ES6 (when there are no imports/exports) generates code that can be run in a web browser.
"module": "ES6",
"declaration": false
},
"rootDir": ".",
"include": [
"**/*.ts"
],
"exclude": [
"*.test.ts"
]
}
24 changes: 24 additions & 0 deletions packages/server/src/routes/index/sync_debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { ErrorMethodNotAllowed } from '../../utils/errors';
import { contextSessionId } from '../../utils/requestUtils';
import defaultView from '../../utils/defaultView';

const router = new Router(RouteType.Web);

router.get('sync_debug', async (_path: SubPath, ctx: AppContext) => {
contextSessionId(ctx);

if (ctx.method !== 'GET') {
throw new ErrorMethodNotAllowed();
}

const view = defaultView('sync_debug', 'Sync Debug');
view.cssFiles = ['index/sync_debug'];
view.jsFiles = ['index/sync_debug'];
return view;
});

export default router;
2 changes: 2 additions & 0 deletions packages/server/src/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import adminUserDeletions from './admin/user_deletions';
import adminUsers from './admin/users';

import indexChanges from './index/changes';
import indexSyncDebug from './index/sync_debug';
import indexHelp from './index/help';
import indexHome from './index/home';
import indexItems from './index/items';
Expand Down Expand Up @@ -56,6 +57,7 @@ const routes: Routers = {
'admin/users': adminUsers,

'changes': indexChanges,
'sync_debug': indexSyncDebug,
'help': indexHelp,
'home': indexHome,
'items': indexItems,
Expand Down
24 changes: 24 additions & 0 deletions packages/server/src/views/index/sync_debug.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<h1 class="title">Debugging</h1>
<p>
Use these tools to debug sync issues with Joplin Server.
</p>

<div id="debug-tools-container">
<fieldset id="note-on-server-check">
<legend>
Check if an item (note, notebook, resource, tag) is present on the server
</legend>
<label for="note-on-server-input">External link or ID:</label>
<input type="text" id="note-on-server-input"/>
<noscript>JavaScript required</noscript>
</fieldset>

<fieldset id="note-in-diff-check">
<legend>
Check if an item is sent to Joplin to download on an initial sync
</legend>
<label for="note-on-server-input">External link or ID:</label>
<input type="text" id="note-in-diff-input"/>
<noscript>JavaScript required</noscript>
</fieldset>
</div>
3 changes: 2 additions & 1 deletion packages/server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
],
"exclude": [
"**/node_modules",
],
"public/js/"
]
}
3 changes: 2 additions & 1 deletion packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ module.exports = {
'packages/app-mobile/ios/**',
'packages/fork-sax/**',
'packages/lib/plugin_types/**',
'packages/server/**',
'packages/server/src/**',
'packages/server/dist/**',
'packages/utils/**',
],
}).filter(f => !f.endsWith('.d.ts'));
Expand Down
1 change: 1 addition & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"scripts": {
"tsc": "tsc --project tsconfig.json",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
"build": "yarn run tsc",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently needed because yarn run tsc is run after yarn workspaces foreach --verbose --interlaced --topological run build. A script run during yarn ... run build, however, now relies on utils being compiled.

A similar line is added in #9360.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer we keep the existing order - install, build, tsc to keeps things more predictable. So far we managed to do this, so we can probably find a solution here too? In some cases I think we use ts-node to run TypeScript files before they've been built, which would be fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently required to use execCommand in gulpfile.js.

There does seem to be a way to run gulpfiles with ts-node, but if we switch to a different build method, this shouldn't be necessary (see comment below).

It also might not be sufficient — build.ts is run with ts-node in #9360 but still seems to be unable to import execCommand on a first build. I think it's related to @joplin/utils compiling JavaScript into a dist/ directory and not exporting the TypeScript?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we're also running tsc during build for packages/react-native-saf-x and packages/fork-htmlparser2.

I'm not sure using ts-node would help here — #9360 uses ts-node and is experiencing the same issue.

"test": "jest --verbose=false",
"test-ci": "yarn test"
},
Expand Down
Loading