Skip to content

Commit 94ea407

Browse files
author
hknokh2
committed
feat: add --file export selection and diagnostic path masking updates
1 parent 7d930c8 commit 94ea407

File tree

11 files changed

+602
-89
lines changed

11 files changed

+602
-89
lines changed

messages/logging.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,19 +152,25 @@ Execution of the command %s has been completed. Exit code %s (%s).
152152

153153
# commandFailedConfigurationGuidance
154154

155-
To localize the root cause of the issue, first check your migration configuration, because most failures are caused by setup mistakes.
155+
To localize the root cause of the issue, first check your migration configuration.
156+
Most failures are caused by setup mistakes.
156157

157-
If you decide to open an issue in the GitHub issue tracker, please run SFDMU with --diagnostic --anonymise and attach to your issue the full generated .log file from that run.
158+
If you decide to open an issue in the GitHub issue tracker:
159+
160+
- Run SFDMU with `--diagnostic --anonymise`
161+
- Attach the full generated `.log` file from that run
158162

159163
Example:
160164
sf sfdmu run --sourceusername source@name.com --targetusername target@name.com --diagnostic --anonymise
161165

162-
Sensitive data in this log is masked when --anonymise is used.
163-
For exact details, see https://help.sfdmu.com/full-documentation/reports/the-execution-log#what-is-masked-and-what-is-not.
166+
Sensitive data in this log is masked when `--anonymise` is used.
167+
For exact details, see:
168+
https://help.sfdmu.com/full-documentation/reports/the-execution-log#what-is-masked-and-what-is-not
164169

165-
If there are failed rows, please also attach the relevant \_target.csv (with 1-2 full affected rows).
170+
If there are failed rows, please also attach the relevant `_target.csv`
171+
(with 1-2 full affected rows).
166172

167-
Without the full diagnostic log, I cannot investigate and help with this issue.
173+
Without the full diagnostic log, we cannot investigate and help with this issue.
168174

169175
# jobCompletedHeader
170176

messages/sfdmu.run.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ Directory containing export.json.
3232

3333
Defaults to the current working directory.
3434

35+
# flags.file.summary
36+
37+
Path to export.json file.
38+
39+
# flags.file.description
40+
41+
Optionally specifies a custom export.json file to execute.
42+
If relative, it is resolved against --path.
43+
All runtime folders and relative resources still use --path as the base directory.
44+
3545
# flags.silent.summary
3646

3747
Suppress standard output.

src/commands/sfdmu/run.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const mapFlags = (flags: SfdmuRunFlagsType): SfdmuRunFlagsType => ({
2828
sourceusername: flags.sourceusername,
2929
targetusername: flags.targetusername,
3030
path: flags.path,
31+
file: flags.file,
3132
silent: flags.silent,
3233
quiet: flags.quiet,
3334
diagnostic: flags.diagnostic,
@@ -98,6 +99,10 @@ export default class Run extends SfCommand<SfdmuRunResultType> {
9899
summary: messages.getMessage('flags.path.summary'),
99100
description: messages.getMessage('flags.path.description'),
100101
}),
102+
file: Flags.string({
103+
summary: messages.getMessage('flags.file.summary'),
104+
description: messages.getMessage('flags.file.description'),
105+
}),
101106
silent: Flags.boolean({
102107
summary: messages.getMessage('flags.silent.summary'),
103108
description: messages.getMessage('flags.silent.description'),

src/modules/logging/LoggingService.ts

Lines changed: 187 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8+
import * as path from 'node:path';
9+
import { fileURLToPath } from 'node:url';
810
import { Messages } from '@salesforce/core';
911
import { Common } from '../common/Common.js';
1012
import {
@@ -32,6 +34,22 @@ type LogLevelType = (typeof LOG_LEVELS)[keyof typeof LOG_LEVELS];
3234
* Logging service with legacy formatting.
3335
*/
3436
export default class LoggingService {
37+
// ------------------------------------------------------//
38+
// -------------------- STATIC FIELDS ------------------ //
39+
// ------------------------------------------------------//
40+
41+
/**
42+
* Absolute plugin root path resolved from this module location.
43+
*/
44+
private static readonly _PLUGIN_ROOT_PATH = path
45+
.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')
46+
.replace(/\\/g, '/');
47+
48+
/**
49+
* Plugin root folder name used as visible anchor in masked stack paths.
50+
*/
51+
private static readonly _PLUGIN_ROOT_FOLDER = path.basename(LoggingService._PLUGIN_ROOT_PATH);
52+
3553
// ------------------------------------------------------//
3654
// -------------------- PUBLIC FIELDS ------------------ //
3755
// ------------------------------------------------------//
@@ -519,37 +537,146 @@ export default class LoggingService {
519537
}
520538

521539
/**
522-
* Masks absolute folder paths in stack traces while preserving file names and positions.
540+
* Masks stack-trace absolute paths while preserving plugin-relative paths.
523541
*
524542
* @param stack - Raw stack trace text.
525-
* @returns Stack trace with masked folder paths.
543+
* @returns Stack trace with masked leading path segments.
526544
*/
527545
private _maskStackTracePaths(stack: string): string {
528-
void this;
529546
if (!stack) {
530547
return stack;
531548
}
532549

533-
let output = stack;
534-
output = output.replace(
535-
/(file:\/\/\/)(?:[A-Za-z]:\/|\/)?(?:[^:\r\n()]+\/)+([^/:\r\n()]+)(:\d+(?::\d+)?)?/g,
536-
(_full: string, prefix: string, fileName: string, position: string | undefined) =>
537-
`${prefix}<masked-path>/${fileName}${position ?? ''}`
538-
);
539-
output = output.replace(
540-
/\\\\[^\\\r\n]+\\[^\\\r\n]+\\(?:[^\\:\r\n()]+\\)+([^\\:\r\n()]+)(:\d+(?::\d+)?)?/g,
541-
(_full: string, fileName: string, position: string | undefined) => `<masked-path>\\${fileName}${position ?? ''}`
542-
);
550+
const lines = stack.split('\n').map((line) => this._maskStackTraceLine(line));
551+
return lines.join('\n');
552+
}
553+
554+
/**
555+
* Masks absolute path tokens in one stack-trace line.
556+
*
557+
* @param line - Stack trace line.
558+
* @returns Line with masked path prefixes.
559+
*/
560+
private _maskStackTraceLine(line: string): string {
561+
let output = line.replace(/\(([^()]+)\)/g, (_full: string, candidate: string) => {
562+
const masked = this._maskStackPathCandidate(candidate);
563+
return `(${masked})`;
564+
});
543565
output = output.replace(
544-
/[A-Za-z]:\\(?:[^\\:\r\n()]+\\)+([^\\:\r\n()]+)(:\d+(?::\d+)?)?/g,
545-
(_full: string, fileName: string, position: string | undefined) => `<masked-path>\\${fileName}${position ?? ''}`
566+
/^\s*at\s+([^(].+)$/u,
567+
(_full: string, candidate: string) => ` at ${this._maskStackPathCandidate(candidate)}`
546568
);
547-
output = output.replace(
548-
/\/(?!<masked-path>\/)(?:[^/:\r\n()]+\/)+([^/:\r\n()]+)(:\d+(?::\d+)?)?/g,
549-
(_full: string, fileName: string, position: string | undefined) => `<masked-path>/${fileName}${position ?? ''}`
569+
return output;
570+
}
571+
572+
/**
573+
* Masks one stack path candidate and preserves plugin-relative suffix when possible.
574+
*
575+
* @param candidate - Raw stack path candidate.
576+
* @returns Masked candidate.
577+
*/
578+
private _maskStackPathCandidate(candidate: string): string {
579+
void this;
580+
if (!candidate || candidate.includes('<masked-path>')) {
581+
return candidate;
582+
}
583+
584+
if (candidate.startsWith('file:///')) {
585+
const rawPath = candidate.replace(/^file:\/\/\//u, '');
586+
const decodedPath = this._safeDecodeUriComponent(rawPath);
587+
const { pathValue, position } = this._splitStackPathPosition(decodedPath);
588+
const maskedPath = this._maskAbsolutePathPrefix(pathValue);
589+
return `file:///${maskedPath}${position}`;
590+
}
591+
592+
if (!this._isAbsoluteStackPath(candidate)) {
593+
return candidate;
594+
}
595+
596+
const { pathValue, position } = this._splitStackPathPosition(candidate);
597+
const maskedPath = this._maskAbsolutePathPrefix(pathValue);
598+
return `${maskedPath}${position}`;
599+
}
600+
601+
/**
602+
* Determines whether a stack candidate is an absolute filesystem path.
603+
*
604+
* @param candidate - Raw stack candidate.
605+
* @returns True when candidate is absolute path-like.
606+
*/
607+
private _isAbsoluteStackPath(candidate: string): boolean {
608+
void this;
609+
return (
610+
(candidate.length > 2 &&
611+
/[A-Za-z]/u.test(candidate[0]) &&
612+
candidate[1] === ':' &&
613+
['\\', '/'].includes(candidate[2])) ||
614+
candidate.startsWith('\\\\') ||
615+
candidate.startsWith('/')
550616
);
617+
}
551618

552-
return output;
619+
/**
620+
* Splits a path token into the path part and optional line or column suffix.
621+
*
622+
* @param value - Raw token value.
623+
* @returns Separated path and position parts.
624+
*/
625+
private _splitStackPathPosition(value: string): { pathValue: string; position: string } {
626+
void this;
627+
const match = value.match(/^(.*?)(:\d+(?::\d+)?)$/u);
628+
if (!match) {
629+
return { pathValue: value, position: '' };
630+
}
631+
return { pathValue: match[1], position: match[2] };
632+
}
633+
634+
/**
635+
* Masks absolute prefix and keeps plugin-relative suffix when available.
636+
*
637+
* @param rawPath - Raw absolute path.
638+
* @returns Masked path.
639+
*/
640+
private _maskAbsolutePathPrefix(rawPath: string): string {
641+
void this;
642+
const normalizedPath = rawPath
643+
.replace(/\\/g, '/')
644+
.replace(/^\/([A-Za-z]:\/)/u, '$1')
645+
.replace(/\/+/g, '/');
646+
const pluginRoot = LoggingService._PLUGIN_ROOT_PATH;
647+
const pluginRootLower = pluginRoot.toLowerCase();
648+
const normalizedLower = normalizedPath.toLowerCase();
649+
const pluginRootFolder = LoggingService._PLUGIN_ROOT_FOLDER;
650+
651+
if (normalizedLower === pluginRootLower || normalizedLower.startsWith(`${pluginRootLower}/`)) {
652+
const suffix = normalizedPath.slice(pluginRoot.length).replace(/^\/+/u, '');
653+
return suffix ? `<masked-path>/${suffix}` : '<masked-path>';
654+
}
655+
656+
const marker = `/${pluginRootFolder.toLowerCase()}/`;
657+
const markerIndex = normalizedLower.indexOf(marker);
658+
if (markerIndex >= 0) {
659+
const relativeFromRoot = normalizedPath.slice(markerIndex + marker.length);
660+
return `<masked-path>/${relativeFromRoot}`;
661+
}
662+
663+
const fileName = normalizedPath.split('/').filter(Boolean).pop();
664+
return fileName ? `<masked-path>/${fileName}` : '<masked-path>';
665+
}
666+
667+
/**
668+
* Safely decodes URI text for file URL path masking.
669+
*
670+
* @param value - URI text.
671+
* @returns Decoded text or original value on decode errors.
672+
*/
673+
private _safeDecodeUriComponent(value: string): string {
674+
void this;
675+
try {
676+
return decodeURIComponent(value);
677+
} catch {
678+
return value;
679+
}
553680
}
554681

555682
/**
@@ -588,18 +715,10 @@ export default class LoggingService {
588715
if (!message) {
589716
return '';
590717
}
591-
let formatted: string;
592-
switch (level) {
593-
case LOG_LEVELS.ERROR:
594-
formatted = this.getMessage('errorLogTemplate', date, message);
595-
break;
596-
case LOG_LEVELS.WARN:
597-
formatted = this.getMessage('warnLogTemplate', date, message);
598-
break;
599-
default:
600-
formatted = this.getMessage('infoLogTemplate', date, message);
601-
break;
602-
}
718+
const formatted = message
719+
.split('\n')
720+
.map((line) => (line ? this._formatStdoutLineByLevel(level, date, line) : ''))
721+
.join('\n');
603722

604723
if (this.context.jsonEnabled) {
605724
return formatted;
@@ -613,6 +732,25 @@ export default class LoggingService {
613732
return `${colorCode}${formatted}\u001b[0m`;
614733
}
615734

735+
/**
736+
* Formats one stdout line by log level.
737+
*
738+
* @param level - Log level.
739+
* @param date - Formatted date.
740+
* @param messageLine - One message line.
741+
* @returns Formatted line.
742+
*/
743+
private _formatStdoutLineByLevel(level: LogLevelType, date: string, messageLine: string): string {
744+
switch (level) {
745+
case LOG_LEVELS.ERROR:
746+
return this.getMessage('errorLogTemplate', date, messageLine);
747+
case LOG_LEVELS.WARN:
748+
return this.getMessage('warnLogTemplate', date, messageLine);
749+
default:
750+
return this.getMessage('infoLogTemplate', date, messageLine);
751+
}
752+
}
753+
616754
/**
617755
* Resolves the default color for a log level.
618756
*
@@ -666,13 +804,28 @@ export default class LoggingService {
666804
if (!message) {
667805
return '\n';
668806
}
807+
return message
808+
.split('\n')
809+
.map((line) => (line ? this._formatFileLineByLevel(level, date, line) : ''))
810+
.join('\n');
811+
}
812+
813+
/**
814+
* Formats one file-log line by log level.
815+
*
816+
* @param level - Log level.
817+
* @param date - Formatted date.
818+
* @param messageLine - One message line.
819+
* @returns Formatted file log line.
820+
*/
821+
private _formatFileLineByLevel(level: LogLevelType, date: string, messageLine: string): string {
669822
switch (level) {
670823
case LOG_LEVELS.ERROR:
671-
return this.getMessage('errorFileLogTemplate', date, message);
824+
return this.getMessage('errorFileLogTemplate', date, messageLine);
672825
case LOG_LEVELS.WARN:
673-
return this.getMessage('warnFileLogTemplate', date, message);
826+
return this.getMessage('warnFileLogTemplate', date, messageLine);
674827
default:
675-
return this.getMessage('infoFileLogTemplate', date, message);
828+
return this.getMessage('infoFileLogTemplate', date, messageLine);
676829
}
677830
}
678831

0 commit comments

Comments
 (0)