Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c8783d9
feat: add log upload functionality and enhance log export process
originalix Oct 21, 2025
20feb31
refactor: rename log-related types and functions for consistency
originalix Oct 21, 2025
2719eb3
feat: implement log collection and export functionality with zip support
originalix Oct 22, 2025
1f29575
feat: enhance log collection by zipping all log files in the directory
originalix Oct 22, 2025
70fcd76
feat: implement log upload functionality and enhance log export process
originalix Oct 22, 2025
5699f30
feat: enhance log upload error handling with detailed server response
originalix Oct 22, 2025
fc0a282
feat: add event bus support for client log upload progress
originalix Oct 22, 2025
fc23425
feat: implement log collection and upload functionality with error ha…
originalix Oct 22, 2025
6be82ca
feat: implement log upload progress tracking and enhance event bus in…
originalix Oct 22, 2025
27f9f84
feat: add client log upload progress tracking and integrate with even…
originalix Oct 22, 2025
6583516
feat: enhance log upload process with progress tracking and error han…
originalix Oct 22, 2025
34fa67e
feat: refactor log export dialog to improve code organization and reu…
originalix Oct 22, 2025
366beb0
feat: refactor log upload dialog to enhance functionality and improve…
originalix Oct 22, 2025
6604e75
feat: update export logs dialog to accept title parameter and improve…
originalix Oct 22, 2025
70e10dc
feat: enhance log upload process with retry mechanism and error handling
originalix Oct 22, 2025
6cf2dce
feat: refine log upload process by correcting total attempts and enha…
originalix Oct 22, 2025
9876aa6
feat: enhance log upload dialog with improved error handling and user…
originalix Oct 23, 2025
41714ef
chore: i18n
originalix Oct 23, 2025
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 apps/desktop/app/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const ipcMessageKeys = {
CHECK_FOR_UPDATES: 'update/checkForUpdates',
TOUCH_UPDATE_RES_SUCCESS: 'touch/update-res-success',
TOUCH_UPDATE_PROGRESS: 'touch/update-progress',
CLIENT_LOG_UPLOAD_PROGRESS: 'client-log/upload-progress',
// OneKey Touch
TOUCH_RES: 'touch/res',
TOUCH_OPEN_PRIVACY_PANEL: 'touch/openPrivacyPanel',
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/app/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const validChannels = [
ipcMessageKeys.APP_LOCK_NOW,
ipcMessageKeys.TOUCH_UPDATE_RES_SUCCESS,
ipcMessageKeys.TOUCH_UPDATE_PROGRESS,
ipcMessageKeys.CLIENT_LOG_UPLOAD_PROGRESS,
ipcMessageKeys.SHOW_ABOUT_WINDOW,
];

Expand Down
185 changes: 184 additions & 1 deletion packages/kit-bg/src/desktopApis/DesktopApiDev.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { createHash } from 'crypto';
import * as fs from 'fs';
import * as fsPromises from 'fs/promises';
import * as path from 'path';

import { shell } from 'electron';
import AdmZip from 'adm-zip';
import { app, shell } from 'electron';
import logger from 'electron-log/main';
import fetch from 'node-fetch';

import { ipcMessageKeys } from '@onekeyhq/desktop/app/config';
import * as store from '@onekeyhq/desktop/app/libs/store';
import { OneKeyLocalError } from '@onekeyhq/shared/src/errors';
import { ELogUploadStage } from '@onekeyhq/shared/src/logger/types';
import type { IDesktopMainProcessDevOnlyApiParams } from '@onekeyhq/shared/types/desktop';

import type { IDesktopApi } from './instance/IDesktopApi';
Expand Down Expand Up @@ -33,6 +41,181 @@ class DesktopApiDev {
await shell.openPath(path.dirname(logger.transports.file.getFile().path));
}

async collectLoggerDigest(params: { fileBaseName: string }): Promise<{
filePath: string;
fileName: string;
mimeType: string;
sizeBytes: number;
sha256: string;
}> {
if (!params.fileBaseName) {
throw new OneKeyLocalError('fileBaseName is required');
}
const baseName = params.fileBaseName;
const logFilePath = logger.transports.file.getFile().path;
const logDir = path.dirname(logFilePath);
const logFiles = await fsPromises.readdir(logDir);

const zipName = `${baseName}.zip`;
const tempDir = path.join(app.getPath('temp'), '@onekeyhq-desktop-logs');
await fsPromises.mkdir(tempDir, { recursive: true });
const zipPath = path.join(tempDir, zipName);

const zip = new AdmZip();
logFiles
.filter((fileName) => fileName.endsWith('.log'))
.forEach((fileName) => {
zip.addLocalFile(path.join(logDir, fileName), '', fileName);
});
zip.writeZip(zipPath);

const fileBuffer = await fsPromises.readFile(zipPath);
const sizeBytes = fileBuffer.length;
const sha256Hex = createHash('sha256').update(fileBuffer).digest('hex');

return {
filePath: zipPath,
fileName: zipName,
mimeType: 'application/zip',
sizeBytes,
sha256: sha256Hex,
};
}

async uploadLoggerBundle(params: {
uploadUrl: string;
filePath: string;
headers: Record<string, string>;
sizeBytes?: number;
}): Promise<any> {
const { uploadUrl, filePath, headers, sizeBytes } = params;
if (!uploadUrl || !filePath) {
throw new OneKeyLocalError('uploadUrl and filePath are required');
}
const reqHeaders: Record<string, string> = { ...headers };
if (sizeBytes !== undefined && !('content-length' in reqHeaders)) {
reqHeaders['content-length'] = String(sizeBytes);
}

logger.info('[client-log-upload] url:', uploadUrl);
logger.info('[client-log-upload] filePath:', filePath);
logger.info('[client-log-upload] sizeBytes:', sizeBytes);
logger.info('[client-log-upload] headers:', JSON.stringify(reqHeaders));

const curlParts = [`curl -X POST '${uploadUrl}'`];
Object.entries(reqHeaders).forEach(([key, value]) => {
curlParts.push(`-H '${key}: ${value}'`);
});
curlParts.push(`--data-binary '@${filePath}'`);
logger.info('[client-log-upload] curl command:', curlParts.join(' \\\n '));

const totalBytes =
typeof sizeBytes === 'number' && sizeBytes > 0
? sizeBytes
: (await fsPromises.stat(filePath)).size ?? 0;
const sendProgress = ({
stage,
progressPercent,
message,
}: {
stage: ELogUploadStage;
progressPercent?: number;
message?: string;
}) => {
try {
const mainWindow =
this.desktopApi.appUpdate.getMainWindow() ?? undefined;
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(
ipcMessageKeys.CLIENT_LOG_UPLOAD_PROGRESS,
{
stage,
progressPercent,
message,
},
);
}
} catch (error) {
logger.warn('[client-log-upload] failed to send progress', error);
}
};

sendProgress({ stage: ELogUploadStage.Uploading, progressPercent: 0 });

let uploadedBytes = 0;
const fileStream = fs.createReadStream(filePath);
if (totalBytes > 0) {
fileStream.on('data', (chunk) => {
uploadedBytes += chunk.length;
const percent = Math.min(
100,
Math.round((uploadedBytes / totalBytes) * 100),
);
sendProgress({
stage: ELogUploadStage.Uploading,
progressPercent: percent,
});
});
}
fileStream.on('error', (streamError) => {
sendProgress({
stage: ELogUploadStage.Error,
message:
streamError instanceof Error
? streamError.message
: String(streamError),
});
});

try {
const response = await fetch(uploadUrl, {
method: 'POST',
headers: reqHeaders,
body: fileStream as unknown as any,
});
const text = await response.text();
try {
const parsed = JSON.parse(text) as Record<string, any>;
if (typeof parsed.code === 'number') {
if (parsed.code === 0) {
sendProgress({
stage: ELogUploadStage.Success,
progressPercent: 100,
});
} else {
sendProgress({
stage: ELogUploadStage.Error,
message:
(parsed?.data as { message?: string } | undefined)?.message ||
parsed.message,
});
}
} else {
sendProgress({
stage: ELogUploadStage.Success,
progressPercent: 100,
});
}
return parsed;
} catch (error) {
sendProgress({
stage: ELogUploadStage.Error,
message: text,
});
return {
code: response.status,
message: text,
};
}
} catch (error) {
sendProgress({
stage: ELogUploadStage.Error,
message: error instanceof Error ? error.message : String(error),
});
throw error;
}
}

async changeDevTools(isOpen: boolean): Promise<void> {
store.setDevTools(isOpen);
globalThis.$desktopMainAppFunctions?.refreshMenu?.();
Expand Down
15 changes: 15 additions & 0 deletions packages/kit-bg/src/services/ServiceLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import {
backgroundClass,
backgroundMethod,
} from '@onekeyhq/shared/src/background/backgroundDecorators';
import { OneKeyLocalError } from '@onekeyhq/shared/src/errors';
import platformEnv from '@onekeyhq/shared/src/platformEnv';
import { EServiceEndpointEnum } from '@onekeyhq/shared/types/endpoint';
import type { IApiClientResponse } from '@onekeyhq/shared/types/endpoint';

import ServiceBase from './ServiceBase';

Expand Down Expand Up @@ -31,6 +34,18 @@ class ServiceLogger extends ServiceBase {
}
return Promise.resolve(true);
}

@backgroundMethod()
async requestUploadToken(payload: { sizeBytes: number; sha256: string }) {
if (payload.sizeBytes <= 0) {
throw new OneKeyLocalError('Log bundle is empty');
}
const client = await this.getClient(EServiceEndpointEnum.Wallet);
const response = await client.post<
IApiClientResponse<{ uploadToken: string; expiresAt: number }>
>('/wallet/v1/client/log/token', payload);
return response.data.data;
}
}

export default ServiceLogger;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';

import {
Accordion,
Expand All @@ -7,7 +7,14 @@ import {
SizableText,
Stack,
} from '@onekeyhq/components';
import { exportLogs } from '@onekeyhq/kit/src/views/Setting/pages/Tab/exportLogs';
import backgroundApiProxy from '@onekeyhq/kit/src/background/instance/backgroundApiProxy';
import {
collectLogDigest,
exportLogs,
uploadLogBundle,
} from '@onekeyhq/kit/src/views/Setting/pages/Tab/exportLogs';
import { appEventBus } from '@onekeyhq/shared/src/eventBus/appEventBus';
import { EAppEventBusNames } from '@onekeyhq/shared/src/eventBus/appEventBusNames';
import { defaultLogger } from '@onekeyhq/shared/src/logger/logger';
import perfUtils, {
EPerformanceTimerLogNames,
Expand All @@ -17,9 +24,41 @@ import LoggingConfigCheckbox from './LoggerConfigGallery';
import { Layout } from './utils/Layout';

const LoggerDemo = () => {
useEffect(() => {
const handler = ({ stage, progressPercent, message }: any) => {
console.log(
'[LoggerDemo][upload-progress]',
stage,
progressPercent,
message,
);
};
appEventBus.on(EAppEventBusNames.ClientLogUploadProgress, handler);
return () => {
appEventBus.off(EAppEventBusNames.ClientLogUploadProgress, handler);
};
}, []);

const downloadLog = useCallback(() => {
void exportLogs('onekey_logs');
}, []);

const uploadLog = useCallback(async () => {
const digest = await collectLogDigest('onekey_logs');
console.log('Log Digest:', digest);
const token = await backgroundApiProxy.serviceLogger.requestUploadToken({
sizeBytes: digest.sizeBytes,
sha256: digest.sha256,
});
console.log('Upload token:', token);

const res = await uploadLogBundle({
uploadToken: token.uploadToken,
digest,
});
console.log('Upload result:', res);
}, []);

return (
<Stack gap="$2">
<Accordion
Expand Down Expand Up @@ -106,6 +145,7 @@ const LoggerDemo = () => {
Log Browser Tabs
</Button>
<Button onPress={downloadLog}>Download Log</Button>
<Button onPress={uploadLog}>Upload Log</Button>
<Button
onPress={() => {
console.log(
Expand All @@ -130,6 +170,7 @@ const LoggerDemo = () => {
</Accordion.Content>
</Accordion.Item>
</Accordion>
<Button onPress={uploadLog}>Upload Log</Button>
</Stack>
);
};
Expand Down
Loading
Loading