Skip to content

Commit 1b80d03

Browse files
author
hknokh2
committed
feat: add skipped-update reason breakdown and diagnostic summary logging
1 parent 497d964 commit 1b80d03

4 files changed

Lines changed: 226 additions & 4 deletions

File tree

messages/logging.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ Nothing was updated.
736736

737737
# skippedUpdatesWarning
738738

739-
{%s} %s target records remained untouched, since they do not differ from the corresponding source records.
739+
{%s} %s target records remained untouched. Same data: %s. No matching target: %s. Other reasons: %s.
740740

741741
# skippedTargetRecordsFilterWarning
742742

src/modules/models/job/MigrationJobTask.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -701,17 +701,23 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
701701

702702
let totalProcessed = 0;
703703
let totalNonProcessed = 0;
704+
let totalSkippedBecauseSameData = 0;
705+
let totalSkippedBecauseNoMatchingTarget = 0;
704706

705707
const businessPass = await this._updateRecordsForPassAsync(updateMode, warnMissingParentsAsync, false);
706708
totalProcessed += businessPass.processedCount;
707709
totalNonProcessed += businessPass.nonProcessedCount;
710+
totalSkippedBecauseSameData += businessPass.skippedBecauseSameDataCount;
711+
totalSkippedBecauseNoMatchingTarget += businessPass.skippedBecauseNoMatchingTargetCount;
708712
applyPassSummary(businessPass.summary);
709713

710714
if (this._isPersonAccountOrContact()) {
711715
logger.log('updatePersonAccountsAndContacts', this.sObjectName);
712716
const personPass = await this._updateRecordsForPassAsync(updateMode, warnMissingParentsAsync, true);
713717
totalProcessed += personPass.processedCount;
714718
totalNonProcessed += personPass.nonProcessedCount;
719+
totalSkippedBecauseSameData += personPass.skippedBecauseSameDataCount;
720+
totalSkippedBecauseNoMatchingTarget += personPass.skippedBecauseNoMatchingTargetCount;
715721
applyPassSummary(personPass.summary);
716722

717723
if (
@@ -723,7 +729,19 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
723729
}
724730

725731
if (totalNonProcessed > 0) {
726-
logger.log('skippedUpdatesWarning', this.sObjectName, String(totalNonProcessed));
732+
const knownSkipped = totalSkippedBecauseSameData + totalSkippedBecauseNoMatchingTarget;
733+
const otherSkipped = Math.max(0, totalNonProcessed - knownSkipped);
734+
logger.log(
735+
'skippedUpdatesWarning',
736+
this.sObjectName,
737+
String(totalNonProcessed),
738+
String(totalSkippedBecauseSameData),
739+
String(totalSkippedBecauseNoMatchingTarget),
740+
String(otherSkipped)
741+
);
742+
logger.verboseFile(
743+
`[diagnostic] update skipped summary: object=${this.sObjectName} total=${totalNonProcessed} sameData=${totalSkippedBecauseSameData} noMatchingTarget=${totalSkippedBecauseNoMatchingTarget} other=${otherSkipped}`
744+
);
727745
}
728746

729747
logger.verboseFile(`[diagnostic] update complete: processed=${totalProcessed}`);
@@ -749,6 +767,8 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
749767
): Promise<{
750768
processedCount: number;
751769
nonProcessedCount: number;
770+
skippedBecauseSameDataCount: number;
771+
skippedBecauseNoMatchingTargetCount: number;
752772
processedData: ProcessedData;
753773
summary: CrudSummaryType;
754774
}> {
@@ -766,6 +786,8 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
766786
return {
767787
processedCount: 0,
768788
nonProcessedCount: 0,
789+
skippedBecauseSameDataCount: 0,
790+
skippedBecauseNoMatchingTargetCount: 0,
769791
processedData,
770792
summary,
771793
};
@@ -787,6 +809,8 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
787809
return {
788810
processedCount: 0,
789811
nonProcessedCount: 0,
812+
skippedBecauseSameDataCount: 0,
813+
skippedBecauseNoMatchingTargetCount: 0,
790814
processedData,
791815
summary,
792816
};
@@ -796,6 +820,8 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
796820
return {
797821
processedCount: 0,
798822
nonProcessedCount: 0,
823+
skippedBecauseSameDataCount: 0,
824+
skippedBecauseNoMatchingTargetCount: 0,
799825
processedData,
800826
summary,
801827
};
@@ -810,6 +836,8 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
810836
return {
811837
processedCount: records.length,
812838
nonProcessedCount: 0,
839+
skippedBecauseSameDataCount: 0,
840+
skippedBecauseNoMatchingTargetCount: 0,
813841
processedData,
814842
summary,
815843
};
@@ -896,6 +924,8 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
896924
return {
897925
processedCount: totalProcessed,
898926
nonProcessedCount: processedData.nonProcessedRecordsAmount,
927+
skippedBecauseSameDataCount: processedData.skippedBecauseSameDataCount,
928+
skippedBecauseNoMatchingTargetCount: processedData.skippedBecauseNoMatchingTargetCount,
899929
processedData,
900930
summary,
901931
};
@@ -3095,7 +3125,13 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
30953125
): void {
30963126
const mutableSourceRecord = sourceRecord;
30973127
const mutableClonedRecord = clonedRecord;
3098-
if (!targetRecord || !shouldCompare) {
3128+
const mutableProcessedData = processedData;
3129+
if (!targetRecord) {
3130+
mutableProcessedData.skippedBecauseNoMatchingTargetCount += 1;
3131+
return;
3132+
}
3133+
if (!shouldCompare) {
3134+
mutableProcessedData.skippedBecauseSameDataCount += 1;
30993135
return;
31003136
}
31013137
mutableClonedRecord.Id = targetRecord['Id'];
@@ -3128,6 +3164,7 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
31283164
): void {
31293165
const mutableSourceRecord = sourceRecord;
31303166
const mutableClonedRecord = clonedRecord;
3167+
const mutableProcessedData = processedData;
31313168

31323169
const canInsert =
31333170
this.scriptObject.operation === OPERATION.Upsert || this.scriptObject.operation === OPERATION.Insert;
@@ -3143,7 +3180,15 @@ export default class MigrationJobTask implements ISFdmuRunCustomAddonTask {
31433180

31443181
const canUpdate =
31453182
this.scriptObject.operation === OPERATION.Upsert || this.scriptObject.operation === OPERATION.Update;
3146-
if (!targetRecord || !canUpdate || !shouldCompare) {
3183+
if (!targetRecord) {
3184+
mutableProcessedData.skippedBecauseNoMatchingTargetCount += 1;
3185+
return;
3186+
}
3187+
if (!canUpdate) {
3188+
return;
3189+
}
3190+
if (!shouldCompare) {
3191+
mutableProcessedData.skippedBecauseSameDataCount += 1;
31473192
return;
31483193
}
31493194
mutableClonedRecord.Id = targetRecord['Id'];

src/modules/models/job/ProcessedData.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ export default class ProcessedData {
5353
*/
5454
public insertedRecordsSourceToTargetMap: ProcessedRecordMapType = new Map();
5555

56+
/**
57+
* Number of skipped records where source and target data are equal.
58+
*/
59+
public skippedBecauseSameDataCount = 0;
60+
61+
/**
62+
* Number of skipped records where no matching target record was found.
63+
*/
64+
public skippedBecauseNoMatchingTargetCount = 0;
65+
5666
// ------------------------------------------------------//
5767
// -------------------- GETTERS/SETTERS ----------------//
5868
// ------------------------------------------------------//
@@ -83,4 +93,15 @@ export default class ProcessedData {
8393
public get nonProcessedRecordsAmount(): number {
8494
return [...this.clonedToSourceMap.values()].filter((record) => record[__IS_PROCESSED_FIELD_NAME] === false).length;
8595
}
96+
97+
/**
98+
* Returns amount of skipped records not covered by known skip-reason counters.
99+
*
100+
* @returns Other skipped-record count.
101+
*/
102+
public get skippedBecauseOtherReasonsCount(): number {
103+
const known = this.skippedBecauseSameDataCount + this.skippedBecauseNoMatchingTargetCount;
104+
const delta = this.nonProcessedRecordsAmount - known;
105+
return delta > 0 ? delta : 0;
106+
}
86107
}

test/modules/job/migration-job-task.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import LoggingContext from '../../../src/modules/logging/LoggingContext.js';
2121
import LoggingService from '../../../src/modules/logging/LoggingService.js';
2222
import MigrationJobTask from '../../../src/modules/models/job/MigrationJobTask.js';
2323
import MigrationJob from '../../../src/modules/models/job/MigrationJob.js';
24+
import ProcessedData from '../../../src/modules/models/job/ProcessedData.js';
2425
import Script from '../../../src/modules/models/script/Script.js';
2526
import type { LookupIdMapType } from '../../../src/modules/models/script/LookupIdMapType.js';
2627
import ScriptMockField from '../../../src/modules/models/script/ScriptMockField.js';
@@ -2156,4 +2157,159 @@ describe('MigrationJobTask file target output', () => {
21562157
fs.rmSync(tempDir, { recursive: true, force: true });
21572158
}
21582159
});
2160+
2161+
it('tracks skipped reason counters for same-data and no-match records', () => {
2162+
const { task } = createTaskFixture();
2163+
task.scriptObject.operation = OPERATION.Update;
2164+
const processedData = new ProcessedData();
2165+
2166+
const classifyForwardsRecord = (
2167+
task as unknown as {
2168+
_classifyForwardsRecord: (
2169+
sourceRecord: Record<string, unknown>,
2170+
clonedRecord: Record<string, unknown>,
2171+
processedData: ProcessedData,
2172+
targetRecord: Record<string, unknown> | undefined,
2173+
shouldCompare: boolean,
2174+
notInsertableFields: string[],
2175+
notUpdateableFields: string[],
2176+
doNotDeleteIdFieldOnInsert: boolean
2177+
) => void;
2178+
}
2179+
)._classifyForwardsRecord.bind(task);
2180+
2181+
classifyForwardsRecord(
2182+
{ Id: 'a00000000000001' },
2183+
{ Name: 'Same' },
2184+
processedData,
2185+
{ Id: '001000000000001' },
2186+
false,
2187+
[],
2188+
[],
2189+
false
2190+
);
2191+
2192+
classifyForwardsRecord(
2193+
{ Id: 'a00000000000002' },
2194+
{ Name: 'NoTarget' },
2195+
processedData,
2196+
undefined,
2197+
true,
2198+
[],
2199+
[],
2200+
false
2201+
);
2202+
2203+
assert.equal(processedData.skippedBecauseSameDataCount, 1);
2204+
assert.equal(processedData.skippedBecauseNoMatchingTargetCount, 1);
2205+
assert.equal(processedData.recordsToInsert.length, 0);
2206+
assert.equal(processedData.recordsToUpdate.length, 0);
2207+
});
2208+
2209+
it('logs skipped warning with split reason counters', async () => {
2210+
const originalLogger = Common.logger;
2211+
const logger = createLoggingService();
2212+
const logCalls: Array<{ message: string; tokens: string[] }> = [];
2213+
const diagnosticLines: string[] = [];
2214+
const originalLog = logger.log.bind(logger) as (...args: unknown[]) => void;
2215+
const originalVerboseFile = logger.verboseFile.bind(logger) as (...args: unknown[]) => void;
2216+
logger.log = ((message: string, ...tokens: string[]) => {
2217+
logCalls.push({ message, tokens });
2218+
originalLog(message, ...tokens);
2219+
}) as typeof logger.log;
2220+
logger.verboseFile = ((message: string, ...tokens: string[]) => {
2221+
diagnosticLines.push(typeof message === 'string' ? message : String(message));
2222+
originalVerboseFile(message, ...tokens);
2223+
}) as typeof logger.verboseFile;
2224+
2225+
Common.logger = logger;
2226+
const { task } = createTaskFixture();
2227+
task.job.script.logger = logger;
2228+
task.scriptObject.operation = OPERATION.Upsert;
2229+
2230+
const updateRecordsForPassAsync = (
2231+
task as unknown as {
2232+
_updateRecordsForPassAsync: (
2233+
updateMode: 'forwards' | 'backwards',
2234+
warnMissingParentsAsync: ((data: ProcessedData) => Promise<void>) | undefined,
2235+
processPersonAccounts: boolean
2236+
) => Promise<{
2237+
processedCount: number;
2238+
nonProcessedCount: number;
2239+
skippedBecauseSameDataCount: number;
2240+
skippedBecauseNoMatchingTargetCount: number;
2241+
processedData: ProcessedData;
2242+
summary: { inserted: number; updated: number; deleted: number };
2243+
}>;
2244+
}
2245+
)._updateRecordsForPassAsync;
2246+
const isPersonAccountOrContact = (
2247+
task as unknown as {
2248+
_isPersonAccountOrContact: () => boolean;
2249+
}
2250+
)._isPersonAccountOrContact;
2251+
2252+
(
2253+
task as unknown as {
2254+
_updateRecordsForPassAsync: (
2255+
updateMode: 'forwards' | 'backwards',
2256+
warnMissingParentsAsync: ((data: ProcessedData) => Promise<void>) | undefined,
2257+
processPersonAccounts: boolean
2258+
) => Promise<{
2259+
processedCount: number;
2260+
nonProcessedCount: number;
2261+
skippedBecauseSameDataCount: number;
2262+
skippedBecauseNoMatchingTargetCount: number;
2263+
processedData: ProcessedData;
2264+
summary: { inserted: number; updated: number; deleted: number };
2265+
}>;
2266+
_isPersonAccountOrContact: () => boolean;
2267+
}
2268+
)._updateRecordsForPassAsync = async () => ({
2269+
processedCount: 2,
2270+
nonProcessedCount: 5,
2271+
skippedBecauseSameDataCount: 2,
2272+
skippedBecauseNoMatchingTargetCount: 1,
2273+
processedData: new ProcessedData(),
2274+
summary: { inserted: 1, updated: 1, deleted: 0 },
2275+
});
2276+
(
2277+
task as unknown as {
2278+
_isPersonAccountOrContact: () => boolean;
2279+
}
2280+
)._isPersonAccountOrContact = () => false;
2281+
2282+
try {
2283+
const processed = await task.updateRecordsAsync('forwards');
2284+
assert.equal(processed, 2);
2285+
const skippedCall = logCalls.find((item) => item.message === 'skippedUpdatesWarning');
2286+
assert.ok(skippedCall);
2287+
assert.deepEqual(skippedCall?.tokens, ['Account', '5', '2', '1', '2']);
2288+
assert.equal(
2289+
diagnosticLines.some(
2290+
(line) =>
2291+
line.includes('[diagnostic] update skipped summary:') &&
2292+
line.includes('object=Account') &&
2293+
line.includes('total=5') &&
2294+
line.includes('sameData=2') &&
2295+
line.includes('noMatchingTarget=1') &&
2296+
line.includes('other=2')
2297+
),
2298+
true
2299+
);
2300+
} finally {
2301+
(
2302+
task as unknown as {
2303+
_updateRecordsForPassAsync: typeof updateRecordsForPassAsync;
2304+
_isPersonAccountOrContact: typeof isPersonAccountOrContact;
2305+
}
2306+
)._updateRecordsForPassAsync = updateRecordsForPassAsync;
2307+
(
2308+
task as unknown as {
2309+
_isPersonAccountOrContact: typeof isPersonAccountOrContact;
2310+
}
2311+
)._isPersonAccountOrContact = isPersonAccountOrContact;
2312+
Common.logger = originalLogger;
2313+
}
2314+
});
21592315
});

0 commit comments

Comments
 (0)