Merge pull request #49 from glitternetwork/fix-check_status

Fix check status
This commit is contained in:
Rr
2026-05-25 22:33:11 +08:00
committed by GitHub
23 changed files with 1093 additions and 70 deletions
+2 -1
View File
@@ -7,4 +7,5 @@ node_modules
/test-repro
.catpaw
.claude/settings.local.json
chrome-test-1
chrome-test-1
docs
+53
View File
@@ -17,6 +17,15 @@ import {
validateDnsDomain,
} from './utils/domainValidator';
import { uploadPath } from './services/uploadService';
import tracker, {
getPathKind,
getTrackErrorReason,
} from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
interface Args {
domain?: string;
@@ -114,6 +123,8 @@ export default async function bindCmd(): Promise<void> {
// Auto-detect domain type if not explicitly specified
const isDns = dns || isDnsDomain(domain);
const displayDomain = normalizeDomain(domain);
const pathKind = getPathKind(path.resolve(targetPath));
const domainType = isDns ? 'dns' : 'pinme_subdomain';
// Validate DNS domain format
if (isDns) {
@@ -165,9 +176,20 @@ export default async function bindCmd(): Promise<void> {
console.log(chalk.blue(`Uploading: ${absolutePath}`));
const up = await uploadPath(absolutePath, { uid: authConfig.address });
if (!up?.contentHash) {
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
path_kind: pathKind,
has_domain: true,
reason: 'no_content_hash',
});
console.log(chalk.red('Upload failed, binding aborted.'));
return;
}
void tracker.trackEvent(TRACK_EVENTS.uploadSuccess, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadSuccess),
path_kind: pathKind,
has_domain: true,
});
console.log(chalk.green(`Upload success, CID: ${up.contentHash}`));
// Bind domain
@@ -182,9 +204,22 @@ export default async function bindCmd(): Promise<void> {
authConfig.token,
);
if (dnsResult.code !== 200) {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'bind',
reason: dnsResult.msg || 'dns_bind_failed',
});
console.log(chalk.red(`DNS binding failed: ${dnsResult.msg}`));
return;
}
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'bind',
});
console.log(chalk.green(`DNS bind success: ${displayDomain}`));
console.log(chalk.white(`Visit: https://${displayDomain}`));
console.log(
@@ -197,9 +232,22 @@ export default async function bindCmd(): Promise<void> {
console.log(chalk.blue('Binding Pinme subdomain...'));
const ok = await bindPinmeDomain(displayDomain, up.contentHash);
if (!ok) {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'bind',
reason: 'pinme_bind_failed',
});
console.log(chalk.red('Binding failed. Please try again later.'));
return;
}
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'bind',
});
console.log(chalk.green(`Bind success: ${displayDomain}`));
const rootDomain = await getRootDomain();
console.log(
@@ -213,6 +261,11 @@ export default async function bindCmd(): Promise<void> {
throw e;
}
} catch (e: any) {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
bind_source: 'bind',
reason: getTrackErrorReason(e),
});
printCliError(e, 'Bind failed.');
}
}
+28
View File
@@ -15,6 +15,12 @@ import {
import { APP_CONFIG, getPinmeApiUrl } from './utils/config';
import { uploadPath } from './services/uploadService';
import { printHighlightedUrl } from './utils/urlDisplay';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
// Template directory - relative to bin folder (works both in dev and npm)
const PROJECT_DIR = process.cwd();
@@ -590,8 +596,30 @@ export default async function createCmd(options: CreateOptions): Promise<void> {
console.log(chalk.gray(` cd ${projectName}`));
console.log(chalk.gray(` pinme save`));
void tracker.trackEvent(
TRACK_EVENTS.projectCreateSuccess,
TRACK_PAGES.project,
{
a: resolveTrackAction(TRACK_EVENTS.projectCreateSuccess),
project_name: workerData.project_name,
template_branch: TEMPLATE_BRANCH,
force: Boolean(options.force),
},
);
process.exit(0);
} catch (error: any) {
void tracker.trackEvent(
TRACK_EVENTS.projectCreateFailed,
TRACK_PAGES.project,
{
a: resolveTrackAction(TRACK_EVENTS.projectCreateFailed),
project_name: options.name,
template_branch: TEMPLATE_BRANCH,
force: Boolean(options.force),
reason: getTrackErrorReason(error),
},
);
printCliError(error, 'Project creation failed.');
process.exit(1);
}
+27
View File
@@ -5,6 +5,12 @@ import fs from 'fs-extra';
import path from 'path';
import { getAuthHeaders } from './utils/webLogin';
import { getPinmeApiUrl } from './utils/config';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
interface DeleteOptions {
name?: string;
@@ -95,6 +101,17 @@ export default async function deleteCmd(options: DeleteOptions): Promise<void> {
const data = response.data;
if (data.code === 200) {
void tracker.trackEvent(
TRACK_EVENTS.projectDeleteSuccess,
TRACK_PAGES.project,
{
a: resolveTrackAction(TRACK_EVENTS.projectDeleteSuccess),
project_name: data.data.project_name,
domain_deleted: Boolean(data.data.domain_deleted),
worker_deleted: Boolean(data.data.worker_deleted),
database_deleted: Boolean(data.data.database_deleted),
},
);
console.log(chalk.green('\n✅ Project deleted successfully!'));
console.log(chalk.gray(`\nProject: ${data.data.project_name}`));
console.log(chalk.gray(` Domain deleted: ${data.data.domain_deleted ? '✅' : '❌'}`));
@@ -109,6 +126,16 @@ export default async function deleteCmd(options: DeleteOptions): Promise<void> {
process.exit(0);
} catch (error: any) {
void tracker.trackEvent(
TRACK_EVENTS.projectDeleteFailed,
TRACK_PAGES.project,
{
a: resolveTrackAction(TRACK_EVENTS.projectDeleteFailed),
project_name: options.name || getProjectName() || undefined,
force: Boolean(options.force),
reason: getTrackErrorReason(error),
},
);
console.log(chalk.red(error));
const errorMsg = error.response?.data?.msg
|| error.message
+30
View File
@@ -10,6 +10,12 @@ import { printCliError } from './utils/cliError';
import { requestCarExport, checkCarExportStatus } from './utils/pinmeApi';
import { getUid } from './utils/getDeviceId';
import { checkNodeVersion } from './utils/checkNodeVersion';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
checkNodeVersion();
@@ -235,6 +241,11 @@ export default async (): Promise<void> => {
);
if (!downloadUrl) {
void tracker.trackEvent(TRACK_EVENTS.exportFailed, TRACK_PAGES.export, {
a: resolveTrackAction(TRACK_EVENTS.exportFailed),
cid,
reason: 'export_failed_or_timed_out',
});
console.log(chalk.red('Export failed or timed out.'));
return;
}
@@ -242,6 +253,11 @@ export default async (): Promise<void> => {
// Step 3: Download CAR file
const success = await downloadCarFile(downloadUrl, finalOutputPath);
if (success) {
void tracker.trackEvent(TRACK_EVENTS.exportSuccess, TRACK_PAGES.export, {
a: resolveTrackAction(TRACK_EVENTS.exportSuccess),
cid,
output_dir: outputDir,
});
const fileSize = fs.statSync(finalOutputPath).size;
const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
console.log(
@@ -254,13 +270,27 @@ export default async (): Promise<void> => {
console.log(chalk.cyan(`Size: ${fileSizeMB} MB`));
console.log(chalk.cyan(`CID: ${cid}`));
} else {
void tracker.trackEvent(TRACK_EVENTS.exportFailed, TRACK_PAGES.export, {
a: resolveTrackAction(TRACK_EVENTS.exportFailed),
cid,
reason: 'download_failed',
});
console.log(chalk.red('Download failed.'));
}
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.exportFailed, TRACK_PAGES.export, {
a: resolveTrackAction(TRACK_EVENTS.exportFailed),
cid,
reason: getTrackErrorReason(error),
});
spinner.fail(`Error: ${error.message}`);
printCliError(error, 'Export failed.');
}
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.exportFailed, TRACK_PAGES.export, {
a: resolveTrackAction(TRACK_EVENTS.exportFailed),
reason: getTrackErrorReason(error),
});
printCliError(error, 'Export failed.');
}
};
+63
View File
@@ -14,6 +14,15 @@ import { getAuthConfig } from './utils/webLogin';
import { APP_CONFIG } from './utils/config';
import { uploadPath } from './services/uploadService';
import { printHighlightedUrl } from './utils/urlDisplay';
import tracker, {
getPathKind,
getTrackErrorReason,
} from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
// get from environment variables
import { checkNodeVersion } from './utils/checkNodeVersion';
@@ -111,6 +120,7 @@ export default async (options?: ImportOptions): Promise<void> => {
console.log(chalk.red(`path ${argPath} does not exist`));
return;
}
const pathKind = getPathKind(absolutePath);
// optional: pre-check domain availability before import
if (domainArg) {
@@ -133,6 +143,11 @@ export default async (options?: ImportOptions): Promise<void> => {
uid: getUid(),
});
if (result) {
void tracker.trackEvent(TRACK_EVENTS.importSuccess, TRACK_PAGES.import, {
a: resolveTrackAction(TRACK_EVENTS.importSuccess),
path_kind: pathKind,
has_domain: Boolean(domainArg),
});
const uid = getUid();
const encryptedCID = encryptHash(
result.contentHash,
@@ -158,6 +173,12 @@ export default async (options?: ImportOptions): Promise<void> => {
);
const ok = await bindPinmeDomain(domainArg, result.contentHash);
if (ok) {
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
domain_type: 'pinme_subdomain',
domain_name: domainArg,
bind_source: 'import',
});
console.log(chalk.green(`Bind success: ${domainArg}`));
const rootDomain = await getRootDomain();
console.log(
@@ -166,12 +187,25 @@ export default async (options?: ImportOptions): Promise<void> => {
),
);
} else {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
domain_type: 'pinme_subdomain',
domain_name: domainArg,
bind_source: 'import',
reason: 'pinme_bind_failed',
});
console.log(chalk.red('Binding failed. Please try again later.'));
}
}
console.log(chalk.green('\n🎉 import successful, program exit'));
}
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.importFailed, TRACK_PAGES.import, {
a: resolveTrackAction(TRACK_EVENTS.importFailed),
path_kind: pathKind,
has_domain: Boolean(domainArg),
reason: getTrackErrorReason(error),
});
printCliError(error, 'Import failed.');
}
process.exit(0);
@@ -192,6 +226,7 @@ export default async (options?: ImportOptions): Promise<void> => {
console.log(chalk.red(`path ${answer.path} does not exist`));
return;
}
const pathKind = getPathKind(absolutePath);
// optional: interactive flow may also parse --domain, reuse the same arg parsing
if (domainArg) {
@@ -215,6 +250,11 @@ export default async (options?: ImportOptions): Promise<void> => {
});
if (result) {
void tracker.trackEvent(TRACK_EVENTS.importSuccess, TRACK_PAGES.import, {
a: resolveTrackAction(TRACK_EVENTS.importSuccess),
path_kind: pathKind,
has_domain: Boolean(domainArg),
});
const uid = getUid();
const encryptedCID = encryptHash(
result.contentHash,
@@ -239,6 +279,12 @@ export default async (options?: ImportOptions): Promise<void> => {
);
const ok = await bindPinmeDomain(domainArg, result.contentHash);
if (ok) {
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
domain_type: 'pinme_subdomain',
domain_name: domainArg,
bind_source: 'import',
});
console.log(chalk.green(`Bind success: ${domainArg}`));
const rootDomain = await getRootDomain();
console.log(
@@ -247,17 +293,34 @@ export default async (options?: ImportOptions): Promise<void> => {
),
);
} else {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
domain_type: 'pinme_subdomain',
domain_name: domainArg,
bind_source: 'import',
reason: 'pinme_bind_failed',
});
console.log(chalk.red('Binding failed. Please try again later.'));
}
}
console.log(chalk.green('\n🎉 import successful, program exit'));
}
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.importFailed, TRACK_PAGES.import, {
a: resolveTrackAction(TRACK_EVENTS.importFailed),
path_kind: pathKind,
has_domain: Boolean(domainArg),
reason: getTrackErrorReason(error),
});
printCliError(error, 'Import failed.');
}
process.exit(0);
}
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.importFailed, TRACK_PAGES.import, {
a: resolveTrackAction(TRACK_EVENTS.importFailed),
reason: getTrackErrorReason(error),
});
printCliError(error, 'Import failed.');
}
};
+20 -1
View File
@@ -2,6 +2,12 @@ import chalk from 'chalk';
import { WebLoginManager } from './utils/webLogin';
import { getDeviceId } from './utils/getDeviceId';
import { bindAnonymousDevice } from './utils/pinmeApi';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
export interface EnvOption {
env?: string;
@@ -14,11 +20,12 @@ const ENV_URLS: Record<string, string> = {
};
export default async function loginCmd(options: EnvOption = {}): Promise<void> {
const env = (options.env || 'prod').toLowerCase();
try {
// Determine web base URL based on env
let webBaseUrl: string | undefined;
// 默认使用 prod 环境
const env = (options.env || 'prod').toLowerCase();
if (ENV_URLS[env]) {
webBaseUrl = ENV_URLS[env];
console.log(chalk.blue(`Using ${env} environment: ${webBaseUrl}`));
@@ -44,9 +51,21 @@ export default async function loginCmd(options: EnvOption = {}): Promise<void> {
console.log(chalk.green('History merged to your account'));
}
void tracker.trackEvent(TRACK_EVENTS.cliLoginSuccess, TRACK_PAGES.login, {
a: resolveTrackAction(TRACK_EVENTS.cliLoginSuccess),
env,
has_token_address: true,
merged_anonymous_history: ok,
});
// Exit the process after successful login
process.exit(0);
} catch (e: any) {
void tracker.trackEvent(TRACK_EVENTS.cliLoginFailed, TRACK_PAGES.login, {
a: resolveTrackAction(TRACK_EVENTS.cliLoginFailed),
env,
reason: getTrackErrorReason(e),
});
console.log(chalk.red(`\nLogin failed: ${e?.message || e}`));
process.exit(1);
}
+14
View File
@@ -1,6 +1,12 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { clearAuthToken, getAuthConfig } from './utils/webLogin';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
export default async function logoutCmd(): Promise<void> {
try {
@@ -28,9 +34,17 @@ export default async function logoutCmd(): Promise<void> {
// Clear auth token
clearAuthToken();
void tracker.trackEvent(TRACK_EVENTS.logoutSuccess, TRACK_PAGES.auth, {
a: resolveTrackAction(TRACK_EVENTS.logoutSuccess),
had_session: true,
});
console.log(chalk.green('Successfully logged out.'));
console.log(chalk.gray(`Address ${auth.address} has been removed from local storage.`));
} catch (e: any) {
void tracker.trackEvent(TRACK_EVENTS.logoutFailed, TRACK_PAGES.auth, {
a: resolveTrackAction(TRACK_EVENTS.logoutFailed),
reason: getTrackErrorReason(e),
});
console.log(chalk.red(`Failed to logout: ${e?.message || e}`));
}
}
+14 -1
View File
@@ -2,10 +2,20 @@ import chalk from 'chalk';
import dayjs from 'dayjs';
import { printCliError } from './utils/cliError';
import { getMyDomains } from './utils/pinmeApi';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
export default async function myDomainsCmd(): Promise<void> {
try {
const list = await getMyDomains();
void tracker.trackEvent(TRACK_EVENTS.myDomainsSuccess, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.myDomainsSuccess),
domain_count: list.length,
});
if (!list.length) {
console.log(chalk.yellow('No bound domains found.'));
return;
@@ -26,7 +36,10 @@ export default async function myDomainsCmd(): Promise<void> {
console.log(chalk.cyan('-'.repeat(80)));
});
} catch (e: any) {
void tracker.trackEvent(TRACK_EVENTS.myDomainsFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.myDomainsFailed),
reason: getTrackErrorReason(e),
});
printCliError(e, 'Failed to fetch domains.');
}
}
+28
View File
@@ -2,6 +2,12 @@ import chalk from 'chalk';
import inquirer from 'inquirer';
import figlet from 'figlet';
import { removeFromIpfs } from './utils/removeFromIpfs';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
interface RemoveOptions {
[key: string]: any;
@@ -97,6 +103,10 @@ export default async (options?: RemoveOptions): Promise<void> => {
try {
const success = await removeFromIpfs(parsedInput.value, parsedInput.type);
if (success) {
void tracker.trackEvent(TRACK_EVENTS.removeSuccess, TRACK_PAGES.remove, {
a: resolveTrackAction(TRACK_EVENTS.removeSuccess),
input_type: parsedInput.type,
});
console.log(
chalk.cyan(
figlet.textSync('Successful', { horizontalLayout: 'full' }),
@@ -104,6 +114,11 @@ export default async (options?: RemoveOptions): Promise<void> => {
);
}
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, {
a: resolveTrackAction(TRACK_EVENTS.removeFailed),
input_type: parsedInput.type,
reason: getTrackErrorReason(error),
});
console.error(chalk.red(`Error: ${error.message}`));
}
return;
@@ -172,6 +187,10 @@ export default async (options?: RemoveOptions): Promise<void> => {
try {
const success = await removeFromIpfs(parsedInput.value, parsedInput.type);
if (success) {
void tracker.trackEvent(TRACK_EVENTS.removeSuccess, TRACK_PAGES.remove, {
a: resolveTrackAction(TRACK_EVENTS.removeSuccess),
input_type: parsedInput.type,
});
console.log(
chalk.cyan(
figlet.textSync('Successful', { horizontalLayout: 'full' }),
@@ -179,10 +198,19 @@ export default async (options?: RemoveOptions): Promise<void> => {
);
}
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, {
a: resolveTrackAction(TRACK_EVENTS.removeFailed),
input_type: parsedInput.type,
reason: getTrackErrorReason(error),
});
console.error(chalk.red(`Error: ${error.message}`));
}
}
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.removeFailed, TRACK_PAGES.remove, {
a: resolveTrackAction(TRACK_EVENTS.removeFailed),
reason: getTrackErrorReason(error),
});
console.error(chalk.red(`Error executing remove command: ${error.message}`));
console.error(error.stack);
}
+20
View File
@@ -24,6 +24,12 @@ import {
import { APP_CONFIG, getPinmeApiUrl } from './utils/config';
import { uploadPath } from './services/uploadService';
import { printHighlightedUrl } from './utils/urlDisplay';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
const PROJECT_DIR = process.cwd();
interface SaveOptions {
@@ -423,8 +429,22 @@ export default async function saveCmd(options: SaveOptions): Promise<void> {
'management',
);
console.log(chalk.green('\nDeployment complete.'));
void tracker.trackEvent(TRACK_EVENTS.projectSaveSuccess, TRACK_PAGES.deploy, {
a: resolveTrackAction(TRACK_EVENTS.projectSaveSuccess),
project_name: projectName,
has_domain: Boolean(options.domain),
domain_type: options.domain
? (isDnsDomain(options.domain) ? 'dns' : 'pinme_subdomain')
: undefined,
});
process.exit(0);
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.projectSaveFailed, TRACK_PAGES.deploy, {
a: resolveTrackAction(TRACK_EVENTS.projectSaveFailed),
project_name: options.projectName || options.name,
has_domain: Boolean(options.domain),
reason: getTrackErrorReason(error),
});
printCliError(error, 'Save failed.');
process.exit(1);
}
+16 -1
View File
@@ -3,6 +3,12 @@ import inquirer from 'inquirer';
import { setAuthToken } from './utils/webLogin';
import { getDeviceId } from './utils/getDeviceId';
import { bindAnonymousDevice } from './utils/pinmeApi';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
export default async function setAppKeyCmd(): Promise<void> {
try {
@@ -33,8 +39,17 @@ export default async function setAppKeyCmd(): Promise<void> {
} else {
console.log(chalk.yellow('Anonymous history merge not confirmed. You may retry later.'));
}
void tracker.trackEvent(TRACK_EVENTS.appKeySetSuccess, TRACK_PAGES.auth, {
a: resolveTrackAction(TRACK_EVENTS.appKeySetSuccess),
merged_anonymous_history: ok,
has_token_address: Boolean(saved.address),
});
} catch (e: any) {
void tracker.trackEvent(TRACK_EVENTS.appKeySetFailed, TRACK_PAGES.auth, {
a: resolveTrackAction(TRACK_EVENTS.appKeySetFailed),
reason: getTrackErrorReason(e),
});
console.log(chalk.red(`Failed to set AppKey: ${e?.message || e}`));
}
}
+18 -1
View File
@@ -1,10 +1,20 @@
import chalk from 'chalk';
import { getAuthConfig } from './utils/webLogin';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
export default function showAppKeyCmd(): void {
try {
const auth = getAuthConfig();
if (!auth) {
void tracker.trackEvent(TRACK_EVENTS.appKeyShownFailed, TRACK_PAGES.auth, {
a: resolveTrackAction(TRACK_EVENTS.appKeyShownFailed),
reason: 'no_appkey_found',
});
console.log(chalk.yellow('No AppKey found. Please set your AppKey first.'));
console.log(chalk.gray('Run: pinme set-appkey <AppKey>'));
return;
@@ -32,8 +42,15 @@ export default function showAppKeyCmd(): void {
} else {
console.log(chalk.cyan(` AppKey: ${'*'.repeat(combined.length)}`));
}
void tracker.trackEvent(TRACK_EVENTS.appKeyShownSuccess, TRACK_PAGES.auth, {
a: resolveTrackAction(TRACK_EVENTS.appKeyShownSuccess),
has_token_address: Boolean(auth.address),
});
} catch (e: any) {
void tracker.trackEvent(TRACK_EVENTS.appKeyShownFailed, TRACK_PAGES.auth, {
a: resolveTrackAction(TRACK_EVENTS.appKeyShownFailed),
reason: getTrackErrorReason(e),
});
console.log(chalk.red(`Failed to show AppKey: ${e?.message || e}`));
}
}
+24
View File
@@ -9,6 +9,12 @@ import {
printCliError,
} from './utils/cliError';
import { getPinmeApiUrl } from './utils/config';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
const PROJECT_DIR = process.cwd();
interface UpdateDbOptions {
@@ -178,8 +184,26 @@ export default async function updateDbCmd(options?: UpdateDbOptions): Promise<vo
await updateDb(sqlFiles, projectName);
console.log(chalk.green('\nDatabase update complete.'));
void tracker.trackEvent(
TRACK_EVENTS.projectUpdateDbSuccess,
TRACK_PAGES.deploy,
{
a: resolveTrackAction(TRACK_EVENTS.projectUpdateDbSuccess),
project_name: projectName,
sql_file_count: sqlFiles.length,
},
);
process.exit(0);
} catch (error: any) {
void tracker.trackEvent(
TRACK_EVENTS.projectUpdateDbFailed,
TRACK_PAGES.deploy,
{
a: resolveTrackAction(TRACK_EVENTS.projectUpdateDbFailed),
project_name: options?.projectName || options?.name,
reason: getTrackErrorReason(error),
},
);
printCliError(error, 'Database update failed.');
process.exit(1);
}
+23
View File
@@ -11,6 +11,12 @@ import {
import { APP_CONFIG } from './utils/config';
import { uploadPath } from './services/uploadService';
import { printHighlightedUrl } from './utils/urlDisplay';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
const PROJECT_DIR = process.cwd();
@@ -123,8 +129,25 @@ export default async function updateWebCmd(options?: UpdateWebOptions): Promise<
await deployFrontend(projectName);
console.log(chalk.green('\nWeb update complete.'));
void tracker.trackEvent(
TRACK_EVENTS.projectUpdateWebSuccess,
TRACK_PAGES.deploy,
{
a: resolveTrackAction(TRACK_EVENTS.projectUpdateWebSuccess),
project_name: projectName,
},
);
process.exit(0);
} catch (error: any) {
void tracker.trackEvent(
TRACK_EVENTS.projectUpdateWebFailed,
TRACK_PAGES.deploy,
{
a: resolveTrackAction(TRACK_EVENTS.projectUpdateWebFailed),
project_name: options?.projectName || options?.name,
reason: getTrackErrorReason(error),
},
);
printCliError(error, 'Web update failed.');
process.exit(1);
}
+24
View File
@@ -11,6 +11,12 @@ import {
printCliError,
} from './utils/cliError';
import { getPinmeApiUrl } from './utils/config';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
const PROJECT_DIR = process.cwd();
interface UpdateWorkerOptions {
@@ -235,8 +241,26 @@ export default async function updateWorkerCmd(options?: UpdateWorkerOptions): Pr
await updateWorker(workerJsPath, modulePaths, metadata, projectName);
console.log(chalk.green('\nWorker update complete.'));
void tracker.trackEvent(
TRACK_EVENTS.projectUpdateWorkerSuccess,
TRACK_PAGES.deploy,
{
a: resolveTrackAction(TRACK_EVENTS.projectUpdateWorkerSuccess),
project_name: projectName,
module_count: modulePaths.length,
},
);
process.exit(0);
} catch (error: any) {
void tracker.trackEvent(
TRACK_EVENTS.projectUpdateWorkerFailed,
TRACK_PAGES.deploy,
{
a: resolveTrackAction(TRACK_EVENTS.projectUpdateWorkerFailed),
project_name: options?.projectName || options?.name,
reason: getTrackErrorReason(error),
},
);
printCliError(error, 'Worker update failed.');
process.exit(1);
}
+168 -11
View File
@@ -19,6 +19,15 @@ import {
import { printCliError, printRechargeUrl } from './utils/cliError';
import { resolveUploadUrls, uploadPath } from './services/uploadService';
import { printHighlightedUrl } from './utils/urlDisplay';
import tracker, {
getPathKind,
getTrackErrorReason,
} from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
import { checkNodeVersion } from './utils/checkNodeVersion';
checkNodeVersion();
@@ -44,13 +53,15 @@ interface UploadOptions {
[key: string]: any;
}
async function printUploadUrls(result: {
contentHash: string;
shortUrl?: string;
pinmeUrl?: string;
dnsUrl?: string;
}): Promise<void> {
const projectName = APP_CONFIG.pinmeProjectName;
async function printUploadUrls(
result: {
contentHash: string;
shortUrl?: string;
pinmeUrl?: string;
dnsUrl?: string;
},
projectName?: string,
): Promise<void> {
const { publicUrl, managementUrl } = await resolveUploadUrls(
result.contentHash,
{
@@ -65,6 +76,77 @@ async function printUploadUrls(result: {
printHighlightedUrl('Management URL', managementUrl, 'management');
}
function readProjectNameFromConfig(configPath: string): string | undefined {
try {
const config = fs.readFileSync(configPath, 'utf-8');
const match = config.match(/project_name\s*=\s*"([^"]+)"/);
return match?.[1]?.trim() || undefined;
} catch {
return undefined;
}
}
function findPinmeConfig(startPath: string): string | undefined {
try {
let current = fs.statSync(startPath).isDirectory()
? startPath
: path.dirname(startPath);
while (true) {
const configPath = path.join(current, 'pinme.toml');
if (fs.existsSync(configPath)) {
return configPath;
}
const parent = path.dirname(current);
if (parent === current) {
return undefined;
}
current = parent;
}
} catch {
return undefined;
}
}
function shouldInferProjectNameFromUploadPath(targetPath: string): boolean {
try {
return (
fs.statSync(targetPath).isDirectory() &&
path.basename(targetPath) === 'dist'
);
} catch {
return false;
}
}
function resolveUploadProjectName(targetPath: string): string | undefined {
if (APP_CONFIG.pinmeProjectName) {
return APP_CONFIG.pinmeProjectName;
}
if (!shouldInferProjectNameFromUploadPath(targetPath)) {
return undefined;
}
const configPaths = [
findPinmeConfig(targetPath),
findPinmeConfig(process.cwd()),
].filter(
(value, index, values): value is string =>
Boolean(value) && values.indexOf(value) === index,
);
for (const configPath of configPaths) {
const projectName = readProjectNameFromConfig(configPath);
if (projectName) {
return projectName;
}
}
return undefined;
}
function getDomainFromArgs(): string | null {
const args = process.argv.slice(2);
const dIdx = args.findIndex((a) => a === '--domain' || a === '-d');
@@ -113,6 +195,7 @@ async function bindDomain(
authConfig: { address: string; token: string },
): Promise<boolean> {
const displayDomain = normalizeDomain(domain);
const domainType = isDns ? 'dns' : 'pinme_subdomain';
if (isDns) {
// DNS domain binding
@@ -124,9 +207,22 @@ async function bindDomain(
authConfig.token,
);
if (dnsResult.code !== 200) {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'upload',
reason: dnsResult.msg || 'dns_bind_failed',
});
console.log(chalk.red(`DNS binding failed: ${dnsResult.msg}`));
return false;
}
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'upload',
});
console.log(chalk.green(`DNS bind success: ${displayDomain}`));
console.log(chalk.white(`Visit: https://${displayDomain}`));
console.log(
@@ -139,9 +235,22 @@ async function bindDomain(
console.log(chalk.blue('Binding Pinme subdomain...'));
const ok = await bindPinmeDomain(displayDomain, contentHash);
if (!ok) {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'upload',
reason: 'pinme_bind_failed',
});
console.log(chalk.red('Binding failed. Please try again later.'));
return false;
}
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'upload',
});
console.log(chalk.green(`Bind success: ${displayDomain}`));
const rootDomain = await (await import('./utils/pinmeApi')).getRootDomain();
console.log(chalk.white(`Visit: https://${displayDomain}.${rootDomain}`));
@@ -181,6 +290,11 @@ export default async (options?: UploadOptions): Promise<void> => {
console.log(chalk.red(`path ${argPath} does not exist`));
return;
}
const pathKind = getPathKind(absolutePath);
const projectName = resolveUploadProjectName(absolutePath);
if (projectName) {
console.log(chalk.gray(`Project: ${projectName}`));
}
// Auto-detect domain type
const isDns = dnsArg || (domainArg ? isDnsDomain(domainArg) : false);
@@ -243,23 +357,42 @@ export default async (options?: UploadOptions): Promise<void> => {
let result;
try {
result = await uploadPath(absolutePath, {
projectName: APP_CONFIG.pinmeProjectName,
projectName,
uid: authConfig?.address,
});
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
path_kind: pathKind,
has_domain: Boolean(domainArg),
reason: getTrackErrorReason(error),
});
printCliError(error, 'Upload failed.');
process.exit(1);
}
if (!result) {
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
path_kind: pathKind,
has_domain: Boolean(domainArg),
reason: 'no_result_returned',
});
console.error(chalk.red('Upload failed: no result returned'));
process.exit(1);
}
void tracker.trackEvent(TRACK_EVENTS.uploadSuccess, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadSuccess),
path_kind: pathKind,
has_domain: Boolean(domainArg),
project_name: projectName,
});
console.log(
chalk.cyan(figlet.textSync('Successful', { horizontalLayout: 'full' })),
);
await printUploadUrls(result);
await printUploadUrls(result, projectName);
// optional: bind domain after upload
if (domainArg) {
@@ -297,6 +430,11 @@ export default async (options?: UploadOptions): Promise<void> => {
console.log(chalk.red(`path ${answer.path} does not exist`));
return;
}
const pathKind = getPathKind(absolutePath);
const projectName = resolveUploadProjectName(absolutePath);
if (projectName) {
console.log(chalk.gray(`Project: ${projectName}`));
}
// Auto-detect domain type
const isDns = dnsArg || (domainArg ? isDnsDomain(domainArg) : false);
@@ -359,23 +497,42 @@ export default async (options?: UploadOptions): Promise<void> => {
let result;
try {
result = await uploadPath(absolutePath, {
projectName: APP_CONFIG.pinmeProjectName,
projectName,
uid: authConfig?.address,
});
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
path_kind: pathKind,
has_domain: Boolean(domainArg),
reason: getTrackErrorReason(error),
});
printCliError(error, 'Upload failed.');
process.exit(1);
}
if (!result) {
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
path_kind: pathKind,
has_domain: Boolean(domainArg),
reason: 'no_result_returned',
});
console.error(chalk.red('Upload failed: no result returned'));
process.exit(1);
}
void tracker.trackEvent(TRACK_EVENTS.uploadSuccess, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadSuccess),
path_kind: pathKind,
has_domain: Boolean(domainArg),
project_name: projectName,
});
console.log(
chalk.cyan(figlet.textSync('Successful', { horizontalLayout: 'full' })),
);
await printUploadUrls(result);
await printUploadUrls(result, projectName);
if (domainArg) {
console.log(
chalk.blue(
+77 -47
View File
@@ -5,6 +5,12 @@ import dayjs from 'dayjs';
import chalk from 'chalk';
import { formatSize } from './uploadLimits';
import { getRootDomain } from './pinmeApi';
import tracker, { getTrackErrorReason } from './tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './trackerEvents';
// history file path
const HISTORY_DIR = path.join(os.homedir(), '.pinme');
@@ -144,57 +150,72 @@ async function formatHistoryUrl(
// display the upload history
const displayUploadHistory = async (limit: number = 10): Promise<void> => {
const history = getUploadHistory(limit);
if (history.length === 0) {
console.log(chalk.yellow('No upload history found.'));
return;
}
console.log(chalk.cyan('Upload History:'));
console.log(chalk.cyan('-'.repeat(80)));
let rootDomain: string | null = null;
try {
rootDomain = await getRootDomain();
} catch {
rootDomain = null;
}
// Display recent records, up to limit records
const recentHistory = history.slice(-limit);
for (const [index, item] of recentHistory.entries()) {
console.log(chalk.green(`${index + 1}. ${item.filename}`));
console.log(chalk.white(` Path: ${item.path}`));
console.log(chalk.white(` IPFS CID: ${item.contentHash}`));
const preferredUrl =
(await formatHistoryUrl(item.dnsUrl)) ||
(await formatHistoryUrl(item.pinmeUrl, {
appendRootDomain: true,
rootDomain,
})) ||
(await formatHistoryUrl(item.shortUrl, {
appendRootDomain: true,
rootDomain,
}));
if (preferredUrl) {
console.log(chalk.white(` URL: ${preferredUrl}`));
}
console.log(chalk.white(` Size: ${formatSize(item.size)}`));
console.log(chalk.white(` Files: ${item.fileCount}`));
console.log(chalk.white(` Type: ${item.type === 'directory' ? 'Directory' : 'File'}`));
if (item.timestamp) {
console.log(chalk.white(` Date: ${new Date(item.timestamp).toLocaleString()}`));
const history = getUploadHistory(limit);
void tracker.trackEvent(TRACK_EVENTS.uploadHistoryViewed, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadHistoryViewed),
record_count: history.length,
limit,
});
if (history.length === 0) {
console.log(chalk.yellow('No upload history found.'));
return;
}
console.log(chalk.cyan('Upload History:'));
console.log(chalk.cyan('-'.repeat(80)));
let rootDomain: string | null = null;
try {
rootDomain = await getRootDomain();
} catch {
rootDomain = null;
}
// Display recent records, up to limit records
const recentHistory = history.slice(-limit);
for (const [index, item] of recentHistory.entries()) {
console.log(chalk.green(`${index + 1}. ${item.filename}`));
console.log(chalk.white(` Path: ${item.path}`));
console.log(chalk.white(` IPFS CID: ${item.contentHash}`));
const preferredUrl =
(await formatHistoryUrl(item.dnsUrl)) ||
(await formatHistoryUrl(item.pinmeUrl, {
appendRootDomain: true,
rootDomain,
})) ||
(await formatHistoryUrl(item.shortUrl, {
appendRootDomain: true,
rootDomain,
}));
if (preferredUrl) {
console.log(chalk.white(` URL: ${preferredUrl}`));
}
console.log(chalk.white(` Size: ${formatSize(item.size)}`));
console.log(chalk.white(` Files: ${item.fileCount}`));
console.log(chalk.white(` Type: ${item.type === 'directory' ? 'Directory' : 'File'}`));
if (item.timestamp) {
console.log(chalk.white(` Date: ${new Date(item.timestamp).toLocaleString()}`));
}
console.log(chalk.cyan('-'.repeat(80)));
}
// display the statistics
const totalSize = history.reduce((sum, record) => sum + record.size, 0);
const totalFiles = history.reduce((sum, record) => sum + record.fileCount, 0);
console.log(chalk.bold(`Total Uploads: ${history.length}`));
console.log(chalk.bold(`Total Files: ${totalFiles}`));
console.log(chalk.bold(`Total Size: ${formatSize(totalSize)}`));
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.uploadHistoryFailed, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadHistoryFailed),
action: 'view',
reason: getTrackErrorReason(error),
});
throw error;
}
// display the statistics
const totalSize = history.reduce((sum, record) => sum + record.size, 0);
const totalFiles = history.reduce((sum, record) => sum + record.fileCount, 0);
console.log(chalk.bold(`Total Uploads: ${history.length}`));
console.log(chalk.bold(`Total Files: ${totalFiles}`));
console.log(chalk.bold(`Total Size: ${formatSize(totalSize)}`));
};
// clear the upload history
@@ -202,9 +223,18 @@ const clearUploadHistory = (): boolean => {
try {
ensureHistoryDir();
fs.writeJsonSync(HISTORY_FILE, { uploads: [] });
void tracker.trackEvent(TRACK_EVENTS.uploadHistoryCleared, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadHistoryCleared),
cleared: true,
});
console.log(chalk.green('Upload history cleared successfully.'));
return true;
} catch (error: any) {
void tracker.trackEvent(TRACK_EVENTS.uploadHistoryFailed, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadHistoryFailed),
action: 'clear',
reason: getTrackErrorReason(error),
});
console.error(chalk.red(`Error clearing upload history: ${error.message}`));
return false;
}
+267
View File
@@ -0,0 +1,267 @@
import os from 'os';
import fs from 'fs';
import path from 'path';
import { spawn } from 'child_process';
import { version } from '../../package.json';
import { getUid } from './getDeviceId';
export interface TrackData {
[key: string]: string | number | boolean | null | undefined;
}
const REQUEST_TIMEOUT_MS = 1500;
const DEFAULT_GATEWAY = 'https://pinme.dev';
const DEFAULT_PRODUCT = 'pinme-cli';
const ACTION_OVERRIDES: Record<string, string> = {
cli_login_success: 'success',
cli_login_failed: 'fail',
appkey_set_success: 'success',
appkey_set_failed: 'fail',
appkey_shown_success: 'view',
appkey_shown_failed: 'fail',
my_domains_success: 'view',
my_domains_failed: 'fail',
wallet_balance_success: 'view',
wallet_balance_failed: 'fail',
upload_history_viewed: 'view',
upload_history_cleared: 'click',
upload_history_failed: 'fail',
};
const TRACK_CHILD_SCRIPT = `
const rawUrl = process.argv[1];
if (!rawUrl) process.exit(0);
try {
const transport = rawUrl.startsWith('https:') ? require('https') : require('http');
const req = transport.get(rawUrl, {
headers: {
'User-Agent': 'Pinme-CLI-Tracker'
}
}, (res) => {
res.resume();
res.on('end', () => process.exit(0));
});
req.setTimeout(${REQUEST_TIMEOUT_MS}, () => req.destroy());
req.on('error', () => process.exit(0));
req.on('close', () => process.exit(0));
} catch (_) {
process.exit(0);
}
`;
function trimTrailingSlash(value: string): string {
return value.replace(/\/+$/, '');
}
function shouldDisableTracking(): boolean {
return (
process.env.PINME_TRACKING_DISABLED === '1' ||
process.env.DO_NOT_TRACK === '1'
);
}
function sanitizeTrackValue(value: unknown): string | undefined {
if (value === undefined || value === null) {
return undefined;
}
return String(value).trim().slice(0, 200);
}
export function getTrackErrorReason(error: unknown): string {
const candidate =
(error as any)?.response?.data?.msg ||
(error as any)?.response?.data?.message ||
(error as any)?.message ||
(error as any)?.toString?.() ||
'unknown_error';
return sanitizeTrackValue(candidate) || 'unknown_error';
}
function resolveTrackAction(event: string, data: TrackData = {}): string {
const explicitAction = data.a;
if (typeof explicitAction === 'string' && explicitAction.trim()) {
return explicitAction.trim();
}
if (ACTION_OVERRIDES[event]) {
return ACTION_OVERRIDES[event];
}
if (event.endsWith('_success')) {
return 'success';
}
if (event.endsWith('_failed') || event.endsWith('_fail')) {
return 'fail';
}
if (event.includes('click') || event.includes('copied')) {
return 'click';
}
if (event.includes('exposure')) {
return 'exposure';
}
if (event.includes('view')) {
return 'view';
}
if (event.endsWith('_started') || event.endsWith('_submit')) {
return 'submit';
}
return 'view';
}
interface ProjectContext {
projectName?: string;
projectDir?: string;
}
let cachedProjectContext: ProjectContext | null = null;
let cachedProjectContextCwd: string | null = null;
function resolveProjectContext(): ProjectContext {
const cwd = process.cwd();
if (cachedProjectContext && cachedProjectContextCwd === cwd) {
return cachedProjectContext;
}
const context: ProjectContext = {};
const configPath = path.join(cwd, 'pinme.toml');
if (fs.existsSync(configPath)) {
try {
const configContent = fs.readFileSync(configPath, 'utf8');
const projectNameMatch = configContent.match(
/project_name\s*=\s*"([^"]+)"/,
);
context.projectName =
sanitizeTrackValue(projectNameMatch?.[1]) ||
sanitizeTrackValue(process.env.PINME_PROJECT_NAME);
context.projectDir = sanitizeTrackValue(path.basename(cwd));
} catch (_) {
context.projectName = sanitizeTrackValue(process.env.PINME_PROJECT_NAME);
}
} else {
context.projectName = sanitizeTrackValue(process.env.PINME_PROJECT_NAME);
}
cachedProjectContext = context;
cachedProjectContextCwd = cwd;
return context;
}
export function getPathKind(pathValue: string): string {
try {
const stat = fs.statSync(pathValue);
if (stat.isDirectory()) {
return 'directory';
}
if (stat.isFile()) {
return 'file';
}
} catch (_) {
return 'unknown';
}
return 'unknown';
}
class Tracker {
private static instance: Tracker;
private readonly gateway: string;
private readonly product: string;
private readonly source: string | undefined;
private readonly disabled: boolean;
private constructor(gateway?: string, product?: string) {
this.gateway = trimTrailingSlash(
gateway || process.env.PINME_TRACKER_GATEWAY || DEFAULT_GATEWAY,
);
this.product = product || DEFAULT_PRODUCT;
this.source = sanitizeTrackValue(process.env.PINME_TRACK_SOURCE);
this.disabled = shouldDisableTracking();
}
public static getInstance(gateway?: string, product?: string): Tracker {
if (!Tracker.instance) {
Tracker.instance = new Tracker(gateway, product);
}
return Tracker.instance;
}
public trackEvent(
event: string,
page: string,
data: TrackData = {},
): Promise<void> {
if (this.disabled || !this.gateway) {
return Promise.resolve();
}
try {
const payload = this.buildPayload(event, page, data);
const params = new URLSearchParams(payload).toString();
const url = `${this.gateway}/track.gif?${params}`;
this.dispatch(url);
} catch (_) {
// Tracking is best-effort and must never interrupt CLI flows.
}
return Promise.resolve();
}
private buildPayload(
event: string,
page: string,
data: TrackData,
): Record<string, string> {
const projectContext = resolveProjectContext();
const action = resolveTrackAction(event, data);
const payload: TrackData = {
...data,
u: getUid(),
s: this.source,
pd: this.product,
p: page,
a: action,
ev: event,
event,
project_name: projectContext.projectName,
project_dir: projectContext.projectDir,
cli_version: version,
node_version: process.version,
os: os.platform(),
arch: os.arch(),
};
const filtered: Record<string, string> = {};
for (const [key, value] of Object.entries(payload)) {
const normalized = sanitizeTrackValue(value);
if (normalized) {
filtered[key] = normalized;
}
}
return filtered;
}
private dispatch(url: string): void {
const child = spawn(process.execPath, ['-e', TRACK_CHILD_SCRIPT, url], {
detached: true,
stdio: 'ignore',
windowsHide: true,
});
child.unref();
}
}
const tracker = Tracker.getInstance();
export default tracker;
+114
View File
@@ -0,0 +1,114 @@
export const TRACK_PAGES = {
auth: 'cli_auth',
login: 'cli_login',
upload: 'cli_upload',
import: 'cli_import',
export: 'cli_export',
remove: 'cli_remove',
domain: 'cli_domain',
wallet: 'cli_wallet',
project: 'cli_project',
deploy: 'cli_deploy',
} as const;
export const TRACK_EVENTS = {
cliLoginSuccess: 'cli_login_success',
cliLoginFailed: 'cli_login_failed',
appKeySetSuccess: 'appkey_set_success',
appKeySetFailed: 'appkey_set_failed',
logoutSuccess: 'logout_success',
logoutFailed: 'logout_failed',
uploadSuccess: 'upload_success',
uploadFailed: 'upload_failed',
importSuccess: 'import_success',
importFailed: 'import_failed',
exportSuccess: 'export_success',
exportFailed: 'export_failed',
removeSuccess: 'remove_success',
removeFailed: 'remove_failed',
domainBindSuccess: 'domain_bind_success',
domainBindFailed: 'domain_bind_failed',
myDomainsSuccess: 'my_domains_success',
myDomainsFailed: 'my_domains_failed',
walletBalanceSuccess: 'wallet_balance_success',
walletBalanceFailed: 'wallet_balance_failed',
projectCreateSuccess: 'project_create_success',
projectCreateFailed: 'project_create_failed',
projectSaveSuccess: 'project_save_success',
projectSaveFailed: 'project_save_failed',
projectUpdateDbSuccess: 'project_update_db_success',
projectUpdateDbFailed: 'project_update_db_failed',
projectUpdateWorkerSuccess: 'project_update_worker_success',
projectUpdateWorkerFailed: 'project_update_worker_failed',
projectDeleteSuccess: 'project_delete_success',
projectDeleteFailed: 'project_delete_failed',
projectUpdateWebSuccess: 'project_update_web_success',
projectUpdateWebFailed: 'project_update_web_failed',
appKeyShownSuccess: 'appkey_shown_success',
appKeyShownFailed: 'appkey_shown_failed',
uploadHistoryViewed: 'upload_history_viewed',
uploadHistoryCleared: 'upload_history_cleared',
uploadHistoryFailed: 'upload_history_failed',
} as const;
export const TRACK_ACTIONS = {
init: 'init',
click: 'click',
view: 'view',
exposure: 'exposure',
submit: 'submit',
success: 'success',
fail: 'fail',
} as const;
export function resolveTrackAction(event: string): string {
switch (event) {
case TRACK_EVENTS.uploadHistoryViewed:
case TRACK_EVENTS.myDomainsSuccess:
case TRACK_EVENTS.walletBalanceSuccess:
case TRACK_EVENTS.appKeyShownSuccess:
return TRACK_ACTIONS.view;
case TRACK_EVENTS.uploadHistoryCleared:
return TRACK_ACTIONS.click;
case TRACK_EVENTS.uploadSuccess:
case TRACK_EVENTS.importSuccess:
case TRACK_EVENTS.exportSuccess:
case TRACK_EVENTS.removeSuccess:
case TRACK_EVENTS.domainBindSuccess:
case TRACK_EVENTS.cliLoginSuccess:
case TRACK_EVENTS.appKeySetSuccess:
case TRACK_EVENTS.logoutSuccess:
case TRACK_EVENTS.projectCreateSuccess:
case TRACK_EVENTS.projectSaveSuccess:
case TRACK_EVENTS.projectUpdateDbSuccess:
case TRACK_EVENTS.projectUpdateWorkerSuccess:
case TRACK_EVENTS.projectUpdateWebSuccess:
case TRACK_EVENTS.projectDeleteSuccess:
return TRACK_ACTIONS.success;
case TRACK_EVENTS.uploadFailed:
case TRACK_EVENTS.importFailed:
case TRACK_EVENTS.exportFailed:
case TRACK_EVENTS.removeFailed:
case TRACK_EVENTS.domainBindFailed:
case TRACK_EVENTS.cliLoginFailed:
case TRACK_EVENTS.appKeySetFailed:
case TRACK_EVENTS.logoutFailed:
case TRACK_EVENTS.myDomainsFailed:
case TRACK_EVENTS.walletBalanceFailed:
case TRACK_EVENTS.projectCreateFailed:
case TRACK_EVENTS.projectSaveFailed:
case TRACK_EVENTS.projectUpdateDbFailed:
case TRACK_EVENTS.projectUpdateWorkerFailed:
case TRACK_EVENTS.projectUpdateWebFailed:
case TRACK_EVENTS.projectDeleteFailed:
case TRACK_EVENTS.appKeyShownFailed:
case TRACK_EVENTS.uploadHistoryFailed:
return TRACK_ACTIONS.fail;
default:
return TRACK_ACTIONS.view;
}
}
+43 -6
View File
@@ -22,6 +22,7 @@ const RETRY_DELAY = APP_CONFIG.upload.retryDelayMs;
const TIMEOUT = APP_CONFIG.upload.timeoutMs;
const MAX_POLL_TIME = APP_CONFIG.upload.maxPollTimeMs;
const POLL_INTERVAL = APP_CONFIG.upload.pollIntervalMs;
const COMPLETE_TO_STATUS_DELAY = 5000;
const PROGRESS_UPDATE_INTERVAL = 200; // ms
const EXPECTED_UPLOAD_TIME = 60000; // 60 seconds
const MAX_PROGRESS = 0.9; // 90%
@@ -142,11 +143,18 @@ function createUploadBusinessError(
);
}
function logAxiosErrorDetails(prefix: string, error: any): void {
function logAxiosErrorDetails(
prefix: string,
error: any,
traceId?: string,
): void {
const status = error?.response?.status;
const responseData = error?.response?.data;
console.error(`[pinme upload] ${prefix}`);
if (traceId) {
console.error(`[pinme upload] trace_id: ${traceId}`);
}
console.error(`[pinme upload] status: ${status ?? 'unknown'}`);
if (responseData === undefined) {
@@ -161,6 +169,20 @@ function logAxiosErrorDetails(prefix: string, error: any): void {
}
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function appendTraceIdToError(error: unknown, traceId: string): Error {
const message = error instanceof Error ? error.message : String(error);
if (message.includes(`trace_id: ${traceId}`)) {
return error instanceof Error ? error : new Error(message);
}
return new Error(`${message} (trace_id: ${traceId})`);
}
function isStorageLimitError(error: any): boolean {
const message = extractAxiosErrorMessage(error).toLowerCase();
return (
@@ -672,10 +694,20 @@ async function getChunkStatus(
);
} catch (error) {
if (axios.isAxiosError(error)) {
logAxiosErrorDetails('up_status failed', error);
throw formatAxiosError('Upload status check failed', error);
logAxiosErrorDetails('up_status failed', error, sessionId);
throw formatAxiosError(
`Upload status check failed (trace_id: ${sessionId})`,
error,
);
}
throw error;
console.error('[pinme upload] up_status failed');
console.error(`[pinme upload] trace_id: ${sessionId}`);
console.error(
`[pinme upload] reason: ${
error instanceof Error ? error.message : String(error)
}`,
);
throw appendTraceIdToError(error, sessionId);
}
}
@@ -686,7 +718,7 @@ async function monitorChunkProgress(
progressBar?: StepProgressBar,
): Promise<UploadResult | null> {
let consecutiveErrors = 0;
const startTime = Date.now();
let startTime = Date.now();
// Start simulating progress
if (progressBar) {
@@ -694,6 +726,9 @@ async function monitorChunkProgress(
}
try {
await delay(COMPLETE_TO_STATUS_DELAY);
startTime = Date.now();
while (Date.now() - startTime < MAX_POLL_TIME) {
try {
const status = await getChunkStatus(traceId, deviceId, options);
@@ -725,7 +760,9 @@ async function monitorChunkProgress(
}
const maxPollTimeMinutes = Math.floor(MAX_POLL_TIME / (60 * 1000));
throw new Error(`Polling timeout after ${maxPollTimeMinutes} minutes`);
throw new Error(
`Polling timeout after ${maxPollTimeMinutes} minutes (trace_id: ${traceId})`,
);
} finally {
// Ensure simulating progress is stopped
if (progressBar) {
+19
View File
@@ -3,6 +3,12 @@ import { printCliError, printRechargeUrl } from './utils/cliError';
import { getWalletRechargeUrl } from './utils/config';
import { getWalletBalance } from './utils/pinmeApi';
import { getAuthConfig } from './utils/webLogin';
import tracker, { getTrackErrorReason } from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
export default async function walletBalanceCmd(): Promise<void> {
try {
@@ -16,10 +22,19 @@ export default async function walletBalanceCmd(): Promise<void> {
const balance = Number(result.data?.wallet_balance_usd ?? 0);
if (!Number.isFinite(balance)) {
void tracker.trackEvent(TRACK_EVENTS.walletBalanceFailed, TRACK_PAGES.wallet, {
a: resolveTrackAction(TRACK_EVENTS.walletBalanceFailed),
reason: 'invalid_balance_value',
});
console.log(chalk.red('Failed to parse wallet balance.'));
return;
}
void tracker.trackEvent(TRACK_EVENTS.walletBalanceSuccess, TRACK_PAGES.wallet, {
a: resolveTrackAction(TRACK_EVENTS.walletBalanceSuccess),
has_balance: balance > 0,
balance_usd: balance.toFixed(2),
});
console.log(chalk.cyan('Wallet balance:'));
console.log(chalk.green(` USD: $${balance.toFixed(2)}`));
@@ -28,6 +43,10 @@ export default async function walletBalanceCmd(): Promise<void> {
printRechargeUrl(getWalletRechargeUrl());
}
} catch (e: any) {
void tracker.trackEvent(TRACK_EVENTS.walletBalanceFailed, TRACK_PAGES.wallet, {
a: resolveTrackAction(TRACK_EVENTS.walletBalanceFailed),
reason: getTrackErrorReason(e),
});
printCliError(e, 'Failed to fetch wallet balance.');
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pinme",
"version": "2.0.4",
"version": "2.0.6",
"publishConfig": {
"access": "public"
},