Improve settings (#168)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Andras Schmelczer 2025-11-19 19:53:10 +00:00 committed by GitHub
parent e75298c4f1
commit c08feba0ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 302 additions and 128 deletions

View file

@ -16,10 +16,14 @@ LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.authors="andras@schmelczer.dev"
COPY --from=builder /build/local-client-cli/dist/cli.js /app/cli.js
COPY --from=builder /build/local-client-cli/dist/healthcheck.js /app/healthcheck.js
HEALTHCHECK --interval=10s --timeout=5s --start-period=60s --retries=1 \
CMD node /app/healthcheck.js /tmp/vaultlink-health.json
WORKDIR /vault
VOLUME ["/vault"]
ENTRYPOINT ["node", "/app/cli.js"]
ENTRYPOINT ["node", "/app/cli.js", "--health", "/tmp/vaultlink-health.json"]
CMD ["--help"]

View file

@ -12,6 +12,8 @@ export interface CliArgs {
ignorePatterns?: string[];
webSocketRetryIntervalMs?: number;
logLevel: LogLevel;
health?: string;
enableTelemetry?: boolean;
}
export function parseArgs(argv: string[]): CliArgs {
@ -51,6 +53,14 @@ export function parseArgs(argv: string[]): CliArgs {
"[OPTIONAL] Log level (DEBUG, INFO, WARNING, ERROR)",
"INFO"
)
.option(
"--health <path>",
"[OPTIONAL] Path to health status file for Docker healthcheck"
)
.option(
"--enable-telemetry",
"[OPTIONAL] Enable telemetry (disabled by default)"
)
.addHelpText(
"after",
`
@ -78,6 +88,8 @@ Examples:
| number
| undefined;
const logLevelStr = (opts.logLevel as string | undefined) ?? "INFO";
const health = opts.health as string | undefined;
const enableTelemetry = opts.enableTelemetry as boolean | undefined;
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
if (localPath === undefined) {
@ -117,6 +129,8 @@ Examples:
maxFileSizeMB: maxFileSizeMb,
ignorePatterns: ignorePattern,
webSocketRetryIntervalMs: websocketRetryIntervalMs,
logLevel
logLevel,
health,
enableTelemetry
};
}

View file

@ -1,5 +1,7 @@
import * as path from "path";
import * as fs from "fs/promises";
import * as fsSync from "fs";
import type { NetworkConnectionStatus } from "sync-client";
import {
SyncClient,
DEFAULT_SETTINGS,
@ -13,6 +15,19 @@ import { FileWatcher } from "./file-watcher";
import { formatLogLine, colorize, styleText } from "./logger-formatter";
import packageJson from "../package.json";
function writeHealthStatus(
filePath: string,
connectionStatus: NetworkConnectionStatus
): void {
try {
fsSync.writeFileSync(filePath, JSON.stringify(connectionStatus));
} catch (error) {
console.error(
`Failed to write health status to ${filePath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
const LOG_LEVEL_ORDER = {
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 1,
@ -78,11 +93,13 @@ async function main(): Promise<void> {
syncConcurrency:
args.syncConcurrency ?? DEFAULT_SETTINGS.syncConcurrency,
maxFileSizeMB: args.maxFileSizeMB ?? DEFAULT_SETTINGS.maxFileSizeMB,
diffCacheSizeMB: DEFAULT_SETTINGS.diffCacheSizeMB,
ignorePatterns,
webSocketRetryIntervalMs:
args.webSocketRetryIntervalMs ??
DEFAULT_SETTINGS.webSocketRetryIntervalMs,
isSyncEnabled: true
isSyncEnabled: true,
enableTelemetry: args.enableTelemetry ?? false
};
const client = await SyncClient.create({
@ -119,6 +136,21 @@ async function main(): Promise<void> {
nativeLineEndings: process.platform === "win32" ? "\r\n" : "\n"
});
if (args.health !== undefined) {
const healthFile = args.health;
const healthInterval = setInterval(() => {
void client.checkConnection().then((status) => {
writeHealthStatus(healthFile, status);
});
}, 30 * 1000); // every 30 seconds
const clearHealthInterval = (): void => {
clearInterval(healthInterval);
};
process.on("SIGINT", clearHealthInterval);
process.on("SIGTERM", clearHealthInterval);
process.on("exit", clearHealthInterval);
}
// Add colored log formatter with level filtering
client.logger.addOnMessageListener((logLine) => {
// Only show messages at or above the configured log level
@ -132,7 +164,10 @@ async function main(): Promise<void> {
const fileWatcher = new FileWatcher(absolutePath, client);
client.addWebSocketStatusChangeListener(() => {
client.logger.info("WebSocket status changed");
const isConnected = client.isWebSocketConnected;
client.logger.info(
`WebSocket status changed: ${isConnected ? "connected" : "disconnected"}`
);
});
client.addRemainingSyncOperationsListener((remaining) => {

View file

@ -0,0 +1,66 @@
#!/usr/bin/env node
/**
* Healthcheck script for Docker container
* Checks if the sync client is connected to the server
*/
import * as fs from "fs";
import type { NetworkConnectionStatus } from "sync-client";
function isHealthStatus(value: unknown): value is NetworkConnectionStatus {
if (typeof value !== "object" || value === null) {
return false;
}
return (
"isSuccessful" in value &&
typeof value.isSuccessful === "boolean" &&
"isWebSocketConnected" in value &&
typeof value.isWebSocketConnected === "boolean" &&
"serverMessage" in value &&
typeof value.serverMessage === "string"
);
}
function main(): void {
if (process.argv.length < 3) {
console.error("Usage: healthcheck <path-to-health-file>");
process.exit(1);
}
const [, , healthFile] = process.argv;
try {
// Check if health file exists
if (!fs.existsSync(healthFile)) {
console.error(`Health file does not exist: ${healthFile}`);
process.exit(1);
}
// Read and parse health status
const content = fs.readFileSync(healthFile, "utf-8");
const parsed: unknown = JSON.parse(content);
// Validate the parsed object using type guard
if (!isHealthStatus(parsed)) {
throw new Error("Invalid health status format");
}
const status = parsed;
if (!status.isSuccessful || !status.isWebSocketConnected) {
console.error("Not connected to server: " + status.serverMessage);
process.exit(1);
}
console.log("Healthy: Connected to server");
process.exit(0);
} catch (error) {
console.error(
`Health check failed: ${error instanceof Error ? error.message : String(error)}`
);
process.exit(1);
}
}
main();

View file

@ -2,7 +2,10 @@ const path = require("path");
const webpack = require("webpack");
module.exports = {
entry: "./src/cli.ts",
entry: {
cli: "./src/cli.ts",
healthcheck: "./src/healthcheck.ts"
},
target: "node",
mode: "production",
optimization: {
@ -21,7 +24,7 @@ module.exports = {
},
output: {
globalObject: "this",
filename: "cli.js",
filename: "[name].js",
path: path.resolve(__dirname, "dist")
},
plugins: [

View file

@ -13,8 +13,6 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@plausible-analytics/tracker": "^0.4.3",
"@sentry/browser": "^10.8.0",
"@types/node": "^24.8.1",
"css-loader": "^7.1.2",
"date-fns": "^4.1.0",
@ -22,7 +20,7 @@
"fs-extra": "^11.3.0",
"mini-css-extract-plugin": "^2.9.2",
"obsidian": "1.10.2",
"reconcile-text": "^0.5.0",
"reconcile-text": "^0.7.1",
"resolve-url-loader": "^5.0.0",
"sass": "^1.91.0",
"sass-loader": "^16.0.5",
@ -33,7 +31,6 @@
"tsx": "^4.20.5",
"typescript": "5.8.3",
"url": "^0.11.4",
"virtual-scroller": "^1.13.1",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1"
}

View file

@ -11,8 +11,6 @@ import { HistoryView } from "./views/history/history-view";
import { StatusBar } from "./views/status-bar/status-bar";
import { LogsView } from "./views/logs/logs-view";
import { StatusDescription } from "./views/status-description/status-description";
import * as Sentry from "@sentry/browser";
import { init as plausibleInit } from "@plausible-analytics/tracker";
import {
SyncClient,
rateLimit,
@ -50,45 +48,6 @@ export default class VaultLinkPlugin extends Plugin {
".trash/**"
);
plausibleInit({
domain: "vault-link",
endpoint: "https://stats.schmelczer.dev/status",
autoCapturePageviews: true,
captureOnLocalhost: true,
logging: true
});
Sentry.init({
dsn: "https://56accd39d92442e788a457a04623cf57@bugs.schmelczer.dev/1",
skipBrowserExtensionCheck: false
});
const onError = (event: ErrorEvent): void => {
Sentry.captureException(event.error, {
extra: {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
}
});
};
window.addEventListener("error", onError);
this.disposables.push(() => {
window.removeEventListener("error", onError);
});
const onUnhandledRejection = (event: PromiseRejectionEvent): void => {
Sentry.captureException(event.reason);
};
window.addEventListener("unhandledrejection", onUnhandledRejection);
this.disposables.push(() => {
window.removeEventListener(
"unhandledrejection",
onUnhandledRejection
);
});
const isDebugBuild = process.env.NODE_ENV === "development";
const debugOptions = isDebugBuild
? {

View file

@ -33,7 +33,7 @@ export class RemoteCursorsPluginValue implements PluginValue {
isOutdated: boolean;
}[] = [];
private static app: App;
private static app?: App;
public decorations: DecorationSet = RangeSet.of([]);
public static setCursors(
@ -88,7 +88,7 @@ export class RemoteCursorsPluginValue implements PluginValue {
private static findFileForEditor(
editor: EditorView
): RelativePath | undefined {
return RemoteCursorsPluginValue.app.workspace
return RemoteCursorsPluginValue.app?.workspace
.getLeavesOfType("markdown")
.map((leaf) => leaf.view)
.filter((view) => view instanceof MarkdownView)

View file

@ -72,6 +72,7 @@ export class SyncSettingsTab extends PluginSettingTab {
this.renderSettingsHeader(containerEl);
this.renderConnectionSettings(containerEl);
this.renderSyncSettings(containerEl);
this.renderMiscSettings(containerEl);
}
public hide(): void {
@ -193,37 +194,27 @@ export class SyncSettingsTab extends PluginSettingTab {
})
);
new Setting(containerEl)
.addButton((button) =>
button.setButtonText("Apply").onClick(async () => {
new Setting(containerEl).addButton((button) =>
button
.setButtonText("Apply & test connection")
.onClick(async () => {
if (this.areThereUnsavedChanges()) {
await this.syncClient.setSettings({
vaultName: this.editedVaultName,
remoteUri: this.editedServerUri,
token: this.editedToken
});
new Notice("Checking connection to the server...");
new Notice(
"The changes have been applied successfully!"
(
await this.syncClient.checkConnection()
).serverMessage
);
await this.statusDescription.updateConnectionState();
} else {
new Notice("No changes to apply");
}
})
)
.addButton((button) =>
button.setButtonText("Test connection").onClick(async () => {
if (this.areThereUnsavedChanges()) {
new Notice(
"There are unsaved changes, testing with the currently saved settings"
);
}
new Notice(
(await this.syncClient.checkConnection()).serverMessage
);
await this.statusDescription.updateConnectionState();
})
);
}
@ -339,6 +330,26 @@ export class SyncSettingsTab extends PluginSettingTab {
);
}
private renderMiscSettings(containerEl: HTMLElement): void {
containerEl.createEl("h3", { text: "Other" });
new Setting(containerEl)
.setName("Enable telemetry")
.setDesc(
"Allow sending anonymous usage data & error reports to help improve the plugin. The data collected is never shared with third parties."
)
.setTooltip(
"Allow sending anonymous usage data & error reports to help improve the plugin. The data collected is never shared with third parties."
)
.addToggle((toggle) =>
toggle
.setValue(this.syncClient.getSettings().enableTelemetry)
.onChange(async (value) =>
this.syncClient.setSetting("enableTelemetry", value)
)
);
}
private setStatusDescriptionSubscription(
newSubscription?: () => unknown
): void {

View file

@ -871,13 +871,6 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@plausible-analytics/tracker": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@plausible-analytics/tracker/-/tracker-0.4.3.tgz",
"integrity": "sha512-RKTgH5xu7Pa77VS4OEnS4woPhDxRgWLJlt9f6JhwgBC9ilknCfJIVEN2A1D8OR7hzgxMQF/hPyls9iN9ReAm3Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@sentry-internal/browser-utils": {
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.8.0.tgz",
@ -3488,10 +3481,9 @@
}
},
"node_modules/reconcile-text": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/reconcile-text/-/reconcile-text-0.5.0.tgz",
"integrity": "sha512-zki3lqw9Oxdhm9ZvDN17VyYoL1Isc8BEL07ILVDE2yGfNEI7thrkczoNCUr+hkFU2rzZtfxECTG0b7p61AJ6wg==",
"dev": true,
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/reconcile-text/-/reconcile-text-0.7.1.tgz",
"integrity": "sha512-khedcYvAKs7ELKh5Z8mz2vyomMY5TqznV1dB+k/7qUAX9cheMNN5/EPJVQYZepOMunYbnQitvhFJX3kD4IMcNw==",
"license": "MIT"
},
"node_modules/regex-parser": {
@ -3499,11 +3491,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/request-animation-frame-timeout": {
"version": "2.0.4",
"dev": true,
"license": "MIT"
},
"node_modules/require-directory": {
"version": "2.1.1",
"dev": true,
@ -4329,14 +4316,6 @@
"resolved": "obsidian-plugin",
"link": true
},
"node_modules/virtual-scroller": {
"version": "1.13.1",
"dev": true,
"license": "MIT",
"dependencies": {
"request-animation-frame-timeout": "^2.0.3"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
@ -4661,8 +4640,6 @@
"version": "0.10.0",
"license": "MIT",
"devDependencies": {
"@plausible-analytics/tracker": "^0.4.3",
"@sentry/browser": "^10.8.0",
"@types/node": "^24.8.1",
"css-loader": "^7.1.2",
"date-fns": "^4.1.0",
@ -4670,7 +4647,7 @@
"fs-extra": "^11.3.0",
"mini-css-extract-plugin": "^2.9.2",
"obsidian": "1.10.2",
"reconcile-text": "^0.5.0",
"reconcile-text": "^0.7.1",
"resolve-url-loader": "^5.0.0",
"sass": "^1.91.0",
"sass-loader": "^16.0.5",
@ -4681,7 +4658,6 @@
"tsx": "^4.20.5",
"typescript": "5.8.3",
"url": "^0.11.4",
"virtual-scroller": "^1.13.1",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1"
}
@ -4696,6 +4672,7 @@
"uuid": "^13.0.0"
},
"devDependencies": {
"@sentry/browser": "^10.8.0",
"@types/node": "^24.8.1",
"ts-loader": "^9.5.2",
"tslib": "2.8.1",
@ -4729,12 +4706,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"sync-client/node_modules/reconcile-text": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/reconcile-text/-/reconcile-text-0.7.1.tgz",
"integrity": "sha512-khedcYvAKs7ELKh5Z8mz2vyomMY5TqznV1dB+k/7qUAX9cheMNN5/EPJVQYZepOMunYbnQitvhFJX3kD4IMcNw==",
"license": "MIT"
},
"test-client": {
"version": "0.10.0",
"bin": {

View file

@ -28,6 +28,7 @@
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1",
"webpack-merge": "^6.0.1",
"@sentry/browser": "^10.8.0",
"ws": "^8.18.3"
}
}

View file

@ -9,6 +9,8 @@ export interface SyncSettings {
maxFileSizeMB: number;
ignorePatterns: string[];
webSocketRetryIntervalMs: number;
diffCacheSizeMB: number;
enableTelemetry: boolean;
}
export const DEFAULT_SETTINGS: SyncSettings = {
@ -19,7 +21,9 @@ export const DEFAULT_SETTINGS: SyncSettings = {
isSyncEnabled: false,
maxFileSizeMB: 10,
ignorePatterns: [],
webSocketRetryIntervalMs: 3500
webSocketRetryIntervalMs: 3500,
diffCacheSizeMB: 4,
enableTelemetry: false
};
export class Settings {

View file

@ -22,11 +22,13 @@ import type { CursorSpan } from "./services/types/CursorSpan";
import type { MaybeOutdatedClientCursors } from "./types/maybe-outdated-client-cursors";
import { FileChangeNotifier } from "./sync-operations/file-change-notifier";
import { FixedSizeDocumentCache } from "./utils/fix-sized-cache";
import { setUpTelemetry } from "./utils/set-up-telemetry";
export class SyncClient {
private static readonly MINIMUM_SAVE_INTERVAL_MS = 1000;
private hasStartedOfflineSync = false;
private hasFinishedOfflineSync = false;
private unloadTelemetry?: () => void;
private constructor(
private readonly history: SyncHistory,
@ -38,8 +40,13 @@ export class SyncClient {
private readonly _logger: Logger,
private readonly connectionStatus: ConnectionStatus,
private readonly cursorTracker: CursorTracker,
private readonly fileChangeNotifier: FileChangeNotifier
private readonly fileChangeNotifier: FileChangeNotifier,
private readonly contentCache: FixedSizeDocumentCache
) {
if (settings.getSettings().enableTelemetry) {
this.unloadTelemetry = setUpTelemetry();
}
this.settings.addOnSettingsChangeListener(
async (newSettings, oldSettings) => {
if (newSettings.vaultName !== oldSettings.vaultName) {
@ -53,6 +60,24 @@ export class SyncClient {
this.stop();
}
}
if (
newSettings.diffCacheSizeMB !== oldSettings.diffCacheSizeMB
) {
this.contentCache.resize(
newSettings.diffCacheSizeMB * 1024 * 1024
);
}
if (
newSettings.enableTelemetry !== oldSettings.enableTelemetry
) {
if (newSettings.enableTelemetry) {
this.unloadTelemetry = setUpTelemetry();
} else {
this.unloadTelemetry?.();
}
}
}
);
}
@ -65,6 +90,10 @@ export class SyncClient {
return this.database.length;
}
public get isWebSocketConnected(): boolean {
return this.webSocketManager.isWebSocketConnected;
}
public static async create({
fs,
persistence,
@ -152,8 +181,7 @@ export class SyncClient {
settings,
syncService,
fileOperations,
unrestrictedSyncer,
contentCache
unrestrictedSyncer
);
const webSocketManager = new WebSocketManager(
@ -182,7 +210,8 @@ export class SyncClient {
logger,
connectionStatus,
cursorTracker,
fileChangeNotifier
fileChangeNotifier,
contentCache
);
logger.info("SyncClient initialised");
@ -235,6 +264,7 @@ export class SyncClient {
public async reset(): Promise<void> {
this.stop();
this.connectionStatus.startReset();
this.contentCache.clear();
await this.syncer.reset();
this.history.reset();
this.database.reset();

View file

@ -34,8 +34,7 @@ export class Syncer {
settings: Settings,
private readonly syncService: SyncService,
private readonly operations: FileOperations,
private readonly internalSyncer: UnrestrictedSyncer,
private readonly contentCache: FixedSizeDocumentCache
private readonly internalSyncer: UnrestrictedSyncer
) {
this.syncQueue = new PQueue({
concurrency: settings.getSettings().syncConcurrency
@ -252,7 +251,6 @@ export class Syncer {
public async reset(): Promise<void> {
await this.waitUntilFinished();
this.contentCache.clear();
}
public async syncRemotelyUpdatedFile(

View file

@ -236,4 +236,40 @@ describe("fixedSizeDocumentCache", () => {
assert.equal(cache.get(2), doc2);
assert.equal(cache.get(3), doc3);
});
it("resizeToLargerSizeNoEviction", async () => {
const cache = new FixedSizeDocumentCache(4);
const doc1 = new Uint8Array([1, 2]);
const doc2 = new Uint8Array([3, 4]);
cache.put(1, doc1);
cache.put(2, doc2);
cache.resize(10);
assert.equal(cache.get(1), doc1);
assert.equal(cache.get(2), doc2);
});
it("resizeCausesMultipleEvictions", async () => {
const cache = new FixedSizeDocumentCache(10);
const doc1 = new Uint8Array([1, 2]);
const doc2 = new Uint8Array([3, 4]);
const doc3 = new Uint8Array([5, 6]);
const doc4 = new Uint8Array([7, 8]);
cache.put(1, doc1);
cache.put(2, doc2);
cache.put(3, doc3);
cache.put(4, doc4);
// Cache has 8 bytes total
cache.resize(2);
// Should evict doc1, doc2, doc3 to get down to 2 bytes
assert.equal(cache.get(1), undefined);
assert.equal(cache.get(2), undefined);
assert.equal(cache.get(3), undefined);
assert.equal(cache.get(4), doc4);
});
});

View file

@ -14,14 +14,12 @@ class LRUNode {
// evicting the least recently used documents when the size limit is exceeded.
export class FixedSizeDocumentCache {
private readonly maxSizeInBytes: number;
private currentSizeInBytes: number;
private readonly cache: Map<VaultUpdateId, LRUNode>;
private head: LRUNode | null; // Least recently used
private tail: LRUNode | null; // Most recently used
public constructor(maxSizeInBytes: number) {
this.maxSizeInBytes = maxSizeInBytes;
public constructor(private maxSizeInBytes: number) {
this.currentSizeInBytes = 0;
this.cache = new Map();
this.head = null;
@ -56,14 +54,7 @@ export class FixedSizeDocumentCache {
this.cache.set(updateId, newNode);
this.addToTail(newNode);
this.currentSizeInBytes += content.byteLength;
// Evict least recently used documents if over size limit
while (this.currentSizeInBytes > this.maxSizeInBytes && this.head) {
const lruNode = this.head;
this.removeNode(lruNode);
this.cache.delete(lruNode.key);
this.currentSizeInBytes -= lruNode.value.byteLength;
}
this.fitBelowMaxSize();
}
public clear(): void {
@ -73,6 +64,21 @@ export class FixedSizeDocumentCache {
this.currentSizeInBytes = 0;
}
public resize(newMaxSizeInBytes: number): void {
this.maxSizeInBytes = newMaxSizeInBytes;
this.fitBelowMaxSize();
}
private fitBelowMaxSize(): void {
// Evict least recently used documents if over size limit
while (this.currentSizeInBytes > this.maxSizeInBytes && this.head) {
const lruNode = this.head;
this.removeNode(lruNode);
this.cache.delete(lruNode.key);
this.currentSizeInBytes -= lruNode.value.byteLength;
}
}
private removeNode(node: LRUNode): void {
if (node.prev) {
node.prev.next = node.next;

View file

@ -0,0 +1,33 @@
import * as Sentry from "@sentry/browser";
export const setUpTelemetry = (): (() => void) => {
Sentry.init({
dsn: "https://56accd39d92442e788a457a04623cf57@bugs.schmelczer.dev/1",
skipBrowserExtensionCheck: false
});
const onError = (event: ErrorEvent): void => {
Sentry.captureException(event.error, {
extra: {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
}
});
};
window.addEventListener("error", onError);
const onUnhandledRejection = (event: PromiseRejectionEvent): void => {
Sentry.captureException(event.reason);
};
window.addEventListener("unhandledrejection", onUnhandledRejection);
return (): void => {
window.removeEventListener("error", onError);
window.removeEventListener("unhandledrejection", onUnhandledRejection);
Sentry.close(5000).catch(() => {
// Ignore errors during shutdown
});
};
};

6
package-lock.json generated Normal file
View file

@ -0,0 +1,6 @@
{
"name": "vault-link",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View file

@ -92,9 +92,9 @@ fn set_up_logging(
.with_line_number(is_debug_mode)
.compact();
let stdout_layer = tracing_subscriber::fmt::layer()
let stderr_layer = tracing_subscriber::fmt::layer()
.with_ansi(use_colors)
.with_writer(std::io::stdout)
.with_writer(std::io::stderr)
.event_format(format.clone());
let file_layer = tracing_subscriber::fmt::layer()
@ -104,8 +104,8 @@ fn set_up_logging(
tracing_subscriber::registry()
.with(env_filter)
.with(stdout_layer)
.with(file_layer)
.with(stderr_layer)
.try_init()
.context("Failed to initialise tracing")
.map_err(init_error)?;