Add lifecycle error and privacy knowledge

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 95c06ad8-377d-4faa-8d07-06300b1c81ec
This commit is contained in:
Jesper Schulz-Wedde 2026-07-14 12:02:49 +02:00
parent 9214f73819
commit 0c2a0ceb82
25 changed files with 457 additions and 37 deletions

View file

@ -7,7 +7,6 @@ codeunit 50190 "Error Type Good Sample"
if not BucketInitialized(BucketId) then begin if not BucketInitialized(BucketId) then begin
InternalErr.ErrorType := ErrorType::Internal; InternalErr.ErrorType := ErrorType::Internal;
InternalErr.Message := StrSubstNo('Ledger bucket %1 was not initialized before posting.', BucketId); InternalErr.Message := StrSubstNo('Ledger bucket %1 was not initialized before posting.', BucketId);
InternalErr.DetailedMessage := 'Internal invariant violated. Inspect the call stack captured in telemetry.';
Error(InternalErr); Error(InternalErr);
end; end;
end; end;

View file

@ -1,5 +1,5 @@
--- ---
bc-version: [all] bc-version: [14..]
domain: error-handling domain: error-handling
keywords: [errorinfo, errortype, internal, client, telemetry, diagnostics, generic-message] keywords: [errorinfo, errortype, internal, client, telemetry, diagnostics, generic-message]
technologies: [al] technologies: [al]
@ -15,7 +15,7 @@ application-area: [all]
## Best Practice ## Best Practice
Reserve `ErrorType::Internal` for errors the user cannot act on: corrupted internal state, an unreachable branch, a contract a caller violated. Set a precise, detail-rich `Message` and `DetailedMessage` for telemetry, raise it via `Error(ErrorInfo)`, and let the platform show the user a generic dialog. Keep `ErrorType::Client` (or a plain `Error`) for failures the user is expected to read and resolve — validation messages, missing setup, business-rule violations. The test is simple: if the message only makes sense to a developer, mark it `Internal`. Reserve `ErrorType::Internal` for errors the user cannot act on: corrupted internal state, an unreachable branch, a contract a caller violated. Set a precise, detail-rich `Message` for telemetry, raise it via `Error(ErrorInfo)`, and let the platform show the user a generic dialog. Keep `ErrorType::Client` (or a plain `Error`) for failures the user is expected to read and resolve — validation messages, missing setup, business-rule violations. The test is simple: if the message only makes sense to a developer, mark it `Internal`.
See sample: `errortype-internal-vs-client-for-diagnostics.good.al`. See sample: `errortype-internal-vs-client-for-diagnostics.good.al`.

View file

@ -0,0 +1,17 @@
codeunit 50301 "Try Return Bad"
{
procedure ImportDocument()
begin
// Ignoring the Boolean result makes this an ordinary, throwing call.
TryImportDocument();
end;
[TryFunction]
local procedure TryImportDocument()
begin
Error(SourceRejectedErr);
end;
var
SourceRejectedErr: Label 'The source document was rejected.';
}

View file

@ -0,0 +1,18 @@
codeunit 50300 "Try Return Good"
{
procedure ImportDocument()
begin
if not TryImportDocument() then
Error(ImportFailedErr);
end;
[TryFunction]
local procedure TryImportDocument()
begin
Error(SourceRejectedErr);
end;
var
ImportFailedErr: Label 'The document could not be imported.';
SourceRejectedErr: Label 'The source document was rejected.';
}

View file

@ -0,0 +1,26 @@
---
bc-version: [13..]
domain: error-handling
keywords: [tryfunction, try-method, boolean-return, ignored-return-value, error-propagation]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Consume a TryFunction return value to enable try semantics
## Description
A procedure marked `[TryFunction]` catches errors only when the caller uses its Boolean return value. An assignment or conditional makes the invocation a try-method call; a bare call is treated as an ordinary procedure call and exposes errors as usual. The attribute alone does not make every invocation non-throwing.
## Best Practice
Consume the result directly: assign it to a Boolean or use the call in an `if` condition. Handle `false` immediately while the last-error state still describes that failure.
See sample: `ignored-tryfunction-return-disables-try-semantics.good.al`.
## Anti Pattern
Calling a `[TryFunction]` procedure as a standalone statement and assuming the attribute suppresses its errors. The call has ordinary error semantics because its Boolean result is ignored.
See sample: `ignored-tryfunction-return-disables-try-semantics.bad.al`.

View file

@ -0,0 +1,13 @@
codeunit 50308 "ErrorInfo Privacy Bad"
{
procedure RaiseSynchronizationError(Customer: Record Customer; ResponseBody: Text)
var
FailureInfo: ErrorInfo;
begin
FailureInfo.Message := StrSubstNo('Synchronization failed for %1.', Customer."E-Mail");
FailureInfo.DataClassification := DataClassification::SystemMetadata;
FailureInfo.ErrorType := ErrorType::Internal;
FailureInfo.DetailedMessage := ResponseBody;
Error(FailureInfo);
end;
}

View file

@ -0,0 +1,17 @@
codeunit 50307 "ErrorInfo Privacy Good"
{
procedure RaiseSynchronizationError()
var
FailureInfo: ErrorInfo;
begin
FailureInfo.Message := SynchronizationFailedErr;
FailureInfo.DataClassification := DataClassification::SystemMetadata;
FailureInfo.ErrorType := ErrorType::Client;
FailureInfo.DetailedMessage := RetryDiagnosticsTxt;
Error(FailureInfo);
end;
var
RetryDiagnosticsTxt: Label 'The remote service rejected the request. Review the integration telemetry event.';
SynchronizationFailedErr: Label 'The synchronization could not be completed.';
}

View file

@ -0,0 +1,26 @@
---
bc-version: [19..]
domain: privacy
keywords: [errorinfo, message, dataclassification, errortype, detailedmessage, copy-details, telemetry]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Review each ErrorInfo text surface by its actual exposure
## Description
`ErrorInfo.Message` is sent to telemetry; with `ErrorType::Client` it is also the primary client message, while `ErrorType::Internal` replaces it in the client with a generic message but still sends the specified text to telemetry. `DataClassification` classifies the content in `Message`; it does not make incorrectly classified personal data safe. `DetailedMessage`, available from BC 19, is omitted from the primary message but is included in the error dialog's **Copy details** content.
## Best Practice
Keep `Message` stable and classify its actual content. Choose `ErrorType` for client usability, not as a telemetry privacy boundary. Put only support-safe technical context in `DetailedMessage`, because a user can copy it from the dialog.
See sample: `errorinfo-telemetry-classification-and-errortype.good.al`.
## Anti Pattern
Marking a dynamic customer-bearing `Message` as `SystemMetadata`, assuming `ErrorType::Internal` keeps it out of telemetry, or placing secrets and personal data in `DetailedMessage` because it is not the primary dialog text.
See sample: `errorinfo-telemetry-classification-and-errortype.bad.al`.

View file

@ -0,0 +1,25 @@
codeunit 50310 "LogError Privacy Bad"
{
procedure SendInvoice()
var
FeatureTelemetry: Codeunit "Feature Telemetry";
CustomDimensions: Dictionary of [Text, Text];
ErrorCallStack: Text;
ErrorText: Text;
begin
if TrySendInvoice() then
exit;
ErrorText := GetLastErrorText();
ErrorCallStack := GetLastErrorCallStack();
CustomDimensions.Add('Operation', 'SendInvoice');
FeatureTelemetry.LogError('0000FT2', 'Invoice exchange', 'Sending invoice',
ErrorText, ErrorCallStack, CustomDimensions);
end;
[TryFunction]
local procedure TrySendInvoice()
begin
Error('Invoice %1 for %2 could not be sent.', 'INV-1001', 'user@example.com');
end;
}

View file

@ -0,0 +1,28 @@
codeunit 50309 "LogError Privacy Good"
{
procedure SendInvoice()
var
FeatureTelemetry: Codeunit "Feature Telemetry";
CustomDimensions: Dictionary of [Text, Text];
ErrorCallStack: Text;
ErrorText: Text;
begin
if TrySendInvoice() then
exit;
ErrorText := GetLastErrorText(true);
ErrorCallStack := GetLastErrorCallStack();
CustomDimensions.Add('Operation', 'SendInvoice');
FeatureTelemetry.LogError('0000FT1', 'Invoice exchange', 'Sending invoice',
ErrorText, ErrorCallStack, CustomDimensions);
end;
[TryFunction]
local procedure TrySendInvoice()
begin
Error(SendFailedErr);
end;
var
SendFailedErr: Label 'The invoice could not be sent.';
}

View file

@ -0,0 +1,26 @@
---
bc-version: [18..]
domain: privacy
keywords: [featuretelemetry, logerror, errortext, errorcallstack, alerrortext, alerrorcallstack, customdimensions]
technologies: [al]
countries: [w1]
application-area: [all]
---
# FeatureTelemetry.LogError emits more than caller custom dimensions
## Description
`FeatureTelemetry.LogError` emits its `ErrorText` as the telemetry message and adds it as `alErrorText`. The overloads with `ErrorCallStack` also add `alErrorCallStack`. These dimensions are produced in addition to the caller-supplied `CustomDimensions` dictionary, and the Feature Telemetry implementation sends the event as `SystemMetadata`.
## Best Practice
Review the dedicated error arguments as telemetry payload. Capture `GetLastErrorText(true)` when scrubbed platform error text is sufficient, and pass `GetLastErrorCallStack()` only as a call stack. Keep custom dimensions non-personal too.
See sample: `featuretelemetry-logerror-implicit-errortext.good.al`.
## Anti Pattern
Approving a `LogError` call because its explicit dictionary contains only safe values while it passes unsanitized `GetLastErrorText()` or arbitrary context through `ErrorText` or `ErrorCallStack`. Those arguments become telemetry dimensions outside the dictionary.
See sample: `featuretelemetry-logerror-implicit-errortext.bad.al`.

View file

@ -0,0 +1,32 @@
codeunit 50303 "Upgrade Phases Bad"
{
Subtype = Upgrade;
trigger OnCheckPreconditionsPerCompany()
begin
// A precondition check must not repair the data it is checking.
RenamePostingGroup();
end;
trigger OnValidateUpgradePerCompany()
begin
// Validation must not perform a migration omitted from OnUpgrade.
MigrateCustomerPostingGroups();
end;
local procedure RenamePostingGroup()
var
CustomerPostingGroup: Record "Customer Posting Group";
begin
if CustomerPostingGroup.Get('OLD') then
CustomerPostingGroup.Rename('NEW');
end;
local procedure MigrateCustomerPostingGroups()
var
Customer: Record Customer;
begin
Customer.SetRange("Customer Posting Group", 'OLD');
Customer.ModifyAll("Customer Posting Group", 'NEW');
end;
}

View file

@ -0,0 +1,63 @@
codeunit 50302 "Upgrade Phases Good"
{
Subtype = Upgrade;
trigger OnCheckPreconditionsPerCompany()
begin
CheckTargetPostingGroup();
end;
trigger OnUpgradePerCompany()
var
UpgradeTag: Codeunit "Upgrade Tag";
begin
if UpgradeTag.HasUpgradeTag(CustomerPostingGroupTag()) then
exit;
MigrateCustomerPostingGroups();
UpgradeTag.SetUpgradeTag(CustomerPostingGroupTag());
end;
trigger OnValidateUpgradePerCompany()
begin
CheckLegacyPostingGroupsRemoved();
end;
local procedure CheckTargetPostingGroup()
var
CustomerPostingGroup: Record "Customer Posting Group";
begin
if not CustomerPostingGroup.Get('NEW') then
Error(TargetGroupMissingErr);
end;
local procedure MigrateCustomerPostingGroups()
var
Customer: Record Customer;
begin
Customer.SetRange("Customer Posting Group", 'OLD');
if Customer.FindSet(true) then
repeat
Customer.Validate("Customer Posting Group", 'NEW');
Customer.Modify(true);
until Customer.Next() = 0;
end;
local procedure CheckLegacyPostingGroupsRemoved()
var
Customer: Record Customer;
begin
Customer.SetRange("Customer Posting Group", 'OLD');
if not Customer.IsEmpty() then
Error(MigrationIncompleteErr);
end;
local procedure CustomerPostingGroupTag(): Code[250]
begin
exit('MS-50302-CustomerPostingGroup-20260714');
end;
var
MigrationIncompleteErr: Label 'The legacy customer posting group was not migrated.';
TargetGroupMissingErr: Label 'Customer posting group NEW must exist before the upgrade.';
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: upgrade
keywords: [on-check-preconditions, on-validate-upgrade, on-upgrade, read-only-check, data-migration]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Upgrade check triggers do not migrate data
## Description
`OnCheckPreconditionsPerCompany`/`PerDatabase` run before the upgrade to verify that it can start. `OnValidateUpgradePerCompany`/`PerDatabase` run after upgrade logic to verify that it succeeded. Treat both phases as read-only checks. The `OnUpgradePerCompany`/`PerDatabase` phase is where the platform expects actual data transformation.
## Best Practice
Have check triggers call query-only helpers that raise an error when an invariant fails. Put every `Insert`, `Modify`, `Delete`, `Rename`, `DataTransfer`, and other migration write behind helpers called from the matching `OnUpgrade...` trigger.
See sample: `check-only-triggers-do-not-migrate-data.good.al`.
## Anti Pattern
Repairing data in `OnCheckPreconditions...` or finishing migration in `OnValidateUpgrade...`. Those writes blur the phase contract and make a check alter the state it is supposed to assess.
See sample: `check-only-triggers-do-not-migrate-data.bad.al`.

View file

@ -4,8 +4,7 @@ codeunit 50211 "Install My Extension"
trigger OnInstallAppPerCompany() trigger OnInstallAppPerCompany()
begin begin
// No DataVersion() guard this runs on every reinstall and upgrade // No DataVersion() guard: a reinstall duplicates seed rows.
// path, duplicating seed rows.
SeedDefaultRows(); SeedDefaultRows();
end; end;

View file

@ -11,20 +11,21 @@ application-area: [all]
## Description ## Description
On the first install of an extension on a tenant the platform records a zero data version: `AppInfo.DataVersion()` returns `Version.Create('0.0.0.0')`. Subsequent upgrades record the actual previous version. The `OnInstallAppPerCompany` trigger uses this distinction to detect a brand-new install — for example, to seed default rows that should not be re-inserted on a normal upgrade. This is the one place where reading `DataVersion()` is the right tool; for everything else, use an upgrade tag. On the first install of an extension on a tenant the platform records a zero data version: `AppInfo.DataVersion()` returns `Version.Create('0.0.0.0')`. During reinstall, `DataVersion()` identifies the previously installed data version. The `OnInstallAppPerCompany` trigger uses this distinction to separate a brand-new install from a reinstall. Ordinary version upgrades do not run install code.
## Best Practice ## Best Practice
In `OnInstallAppPerCompany`, fetch the current `ModuleInfo` via `NavApp.GetCurrentModuleInfo`, compare `AppInfo.DataVersion()` to `Version.Create('0.0.0.0')`, and run install-only seed logic only when they match. On any non-zero data version, exit immediately — that path is an upgrade, not an install. In `OnInstallAppPerCompany`, fetch the current `ModuleInfo` via `NavApp.GetCurrentModuleInfo`, compare `AppInfo.DataVersion()` to `Version.Create('0.0.0.0')`, and run first-install seed logic only when they match. On a non-zero data version, follow the reinstall path or exit.
See sample: `first-install-dataversion-zero-check.good.al`. See sample: `first-install-dataversion-zero-check.good.al`.
## Anti Pattern ## Anti Pattern
Treating `OnInstallAppPerCompany` as if it always implies "fresh tenant". The trigger also fires when reinstalling over an existing data set; without the `0.0.0.0` guard, install-only seed code re-runs on every upgrade and duplicates rows. Treating `OnInstallAppPerCompany` as if it always implies "fresh tenant". The trigger also fires when reinstalling over an existing data set; without the `0.0.0.0` guard, first-install seed code can run again and duplicate rows.
See sample: `first-install-dataversion-zero-check.bad.al`. See sample: `first-install-dataversion-zero-check.bad.al`.
## See also ## See also
- `use-upgrade-tags-not-version-checks.md` — for upgrade steps after first install, use upgrade tags rather than `DataVersion`. - `use-upgrade-tags-not-version-checks.md` — for upgrade steps after first install, use upgrade tags rather than `DataVersion`.
- `install-code-does-not-run-on-version-upgrade.md` — ordinary version upgrades invoke upgrade code, not install code.

View file

@ -0,0 +1,20 @@
codeunit 50306 "My App Install Only"
{
Subtype = Install;
trigger OnInstallAppPerCompany()
begin
// A normal version upgrade never invokes this migration.
MigrateLegacySetup();
end;
local procedure MigrateLegacySetup()
var
MyAppSetup: Record "My App Setup";
begin
if MyAppSetup.Get() then begin
MyAppSetup."Configuration Version" := 2;
MyAppSetup.Modify(true);
end;
end;
}

View file

@ -0,0 +1,48 @@
codeunit 50304 "My App Install"
{
Subtype = Install;
trigger OnInstallAppPerCompany()
begin
InitializeSetup();
end;
local procedure InitializeSetup()
var
MyAppSetup: Record "My App Setup";
begin
if MyAppSetup.IsEmpty() then
MyAppSetup.Insert(true);
end;
}
codeunit 50305 "My App Upgrade"
{
Subtype = Upgrade;
trigger OnUpgradePerCompany()
var
UpgradeTag: Codeunit "Upgrade Tag";
begin
if UpgradeTag.HasUpgradeTag(ConfigurationVersionTag()) then
exit;
MigrateLegacySetup();
UpgradeTag.SetUpgradeTag(ConfigurationVersionTag());
end;
local procedure MigrateLegacySetup()
var
MyAppSetup: Record "My App Setup";
begin
if MyAppSetup.Get() then begin
MyAppSetup."Configuration Version" := 2;
MyAppSetup.Modify(true);
end;
end;
local procedure ConfigurationVersionTag(): Code[250]
begin
exit('MS-50305-ConfigurationVersion-20260714');
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [all]
domain: upgrade
keywords: [install-codeunit, subtype-install, on-install-app, version-upgrade, upgrade-codeunit]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Install code does not run during a version upgrade
## Description
An install codeunit runs when an extension is installed for the first time or an uninstalled version is installed again. Installing a higher extension version through the data-upgrade operation does not invoke `OnInstallAppPerCompany` or `OnInstallAppPerDatabase`. Ordinary version-to-version migration is dispatched only through upgrade codeunits.
## Best Practice
Use `Subtype = Install` for first-install and reinstall initialization. Put version migration in a separate `Subtype = Upgrade` codeunit and enter it from `OnUpgradePerCompany` or `OnUpgradePerDatabase`.
See sample: `install-code-does-not-run-on-version-upgrade.good.al`.
## Anti Pattern
Putting a schema or data migration only in an install trigger and expecting it to run when a higher app version is upgraded. The migration is never invoked on that path.
See sample: `install-code-does-not-run-on-version-upgrade.bad.al`.

View file

@ -4,10 +4,21 @@ codeunit 50235 "Upgrade With Validation"
trigger OnValidateUpgradePerCompany() trigger OnValidateUpgradePerCompany()
begin begin
// No skip logic and no written justification full-table validation // A full-table scan repeats on every upgrade.
// runs on every single upgrade pass.
ValidateAllCustomers(); ValidateAllCustomers();
end; end;
local procedure ValidateAllCustomers() begin end; local procedure ValidateAllCustomers()
var
Customer: Record Customer;
begin
if Customer.FindSet() then
repeat
if Customer."Customer Posting Group" = 'OLD' then
Error(MigrationIncompleteErr);
until Customer.Next() = 0;
end;
var
MigrationIncompleteErr: Label 'The legacy customer posting group was not migrated.';
} }

View file

@ -3,23 +3,19 @@ codeunit 50234 "Upgrade With Validation"
Subtype = Upgrade; Subtype = Upgrade;
trigger OnValidateUpgradePerCompany() trigger OnValidateUpgradePerCompany()
begin
CheckNoLegacyPostingGroups();
end;
local procedure CheckNoLegacyPostingGroups()
var var
UpgradeTag: Codeunit "Upgrade Tag"; Customer: Record Customer;
begin begin
// Justification: regulatory compliance requires a full-table scan once Customer.SetRange("Customer Posting Group", 'OLD');
// per tenant after this release. Tag prevents re-runs. if not Customer.IsEmpty() then
if UpgradeTag.HasUpgradeTag(MyValidationUpgradeTag()) then Error(MigrationIncompleteErr);
exit;
ValidateAllCustomers();
UpgradeTag.SetUpgradeTag(MyValidationUpgradeTag());
end; end;
local procedure ValidateAllCustomers() begin end; var
MigrationIncompleteErr: Label 'The legacy customer posting group was not migrated.';
local procedure MyValidationUpgradeTag(): Code[250]
begin
exit('MS-123456-CustomerValidation-20240101');
end;
} }

View file

@ -1,26 +1,26 @@
--- ---
bc-version: [all] bc-version: [all]
domain: upgrade domain: upgrade
keywords: [on-validate-upgrade-per-company, performance-impact, skip-logic, justification, upgrade-tag] keywords: [on-validate-upgrade-per-company, performance-impact, bounded-query, justification, read-only-check]
technologies: [al] technologies: [al]
countries: [w1] countries: [w1]
application-area: [all] application-area: [all]
--- ---
# Performance-impacting upgrade triggers need justification and skip logic # Keep upgrade validation checks bounded
## Description ## Description
Triggers such as `OnValidateUpgradePerCompany` run on every upgrade pass. When their body performs non-trivial work — full-table scans, cross-table validations — the cost is paid on every upgrade of every tenant, even when there is nothing to validate. That cost is acceptable only when the validation is critical (regulatory compliance, data-integrity guarantees the platform depends on) AND the trigger short-circuits once it has done its work. Triggers such as `OnValidateUpgradePerCompany` run on every upgrade pass. A full-table scan or cross-table validation therefore adds cost to every upgrade of every tenant. Validation is a read-only lifecycle check, so it cannot make itself one-time by writing an upgrade tag.
## Best Practice ## Best Practice
A performance-impacting upgrade trigger carries two things: a written comment that names the reason the work has to happen on every upgrade pass, and an early-exit guard backed by an upgrade tag so the work runs at most once per tenant. The `HasUpgradeTag` check at the top exits when the validation has already been recorded; the `SetUpgradeTag` call at the bottom records completion. Filter directly to invalid rows and use `IsEmpty` or another bounded existence check where possible. If a broad validation is unavoidable, document the invariant that requires it and keep all data changes in `OnUpgrade...`.
See sample: `minimize-onvalidate-upgrade-triggers.good.al`. See sample: `minimize-onvalidate-upgrade-triggers.good.al`.
## Anti Pattern ## Anti Pattern
Doing real work in `OnValidateUpgradePerCompany` with no upgrade-tag guard. The same scan runs every upgrade, multiplying upgrade time by the number of releases the customer takes. Reading every record in `OnValidateUpgradePerCompany` when a filtered existence check can prove the same invariant. The scan repeats on every upgrade.
See sample: `minimize-onvalidate-upgrade-triggers.bad.al`. See sample: `minimize-onvalidate-upgrade-triggers.bad.al`.

View file

@ -38,8 +38,9 @@ Discard files that are not applicable. Retain conditionally applicable files (an
Narrow the relevant files to the subset that applies to the changes under review. For each relevant file, compute overlap against: Narrow the relevant files to the subset that applies to the changes under review. For each relevant file, compute overlap against:
- The changed AL object names and types — especially codeunits that post or validate, tables and table extensions with `OnValidate` triggers, and any procedure that raises errors or orchestrates a batch over records. - The changed AL object names and types — especially codeunits that post or validate, tables and table extensions with `OnValidate` triggers, and any procedure that raises errors or orchestrates a batch over records.
- The changed procedures and triggers, weighted toward `OnValidate`/`OnInsert`/`OnModify` triggers, posting and validation routines, and procedures attributed with `[ErrorBehavior(...)]`. - The changed procedures and triggers, weighted toward `OnValidate`/`OnInsert`/`OnModify` triggers, posting and validation routines, and procedures attributed with `[ErrorBehavior(...)]` or `[TryFunction]`.
- Tokens extracted from the diff that relate to error surfacing and diagnostics (`Error`, `ErrorInfo`, `Title`, `Message`, `DetailedMessage`, `AddAction`, `AddNavigationAction`, `RecordId`, `PageNo`, `ErrorBehavior`, `Collect`, `HasCollectedErrors`, `GetCollectedErrors`, `ClearCollectedErrors`, `ErrorType`, `Internal`, `Client`). - Tokens extracted from the diff that relate to error surfacing and diagnostics (`Error`, `ErrorInfo`, `Title`, `Message`, `DetailedMessage`, `AddAction`, `AddNavigationAction`, `RecordId`, `PageNo`, `ErrorBehavior`, `Collect`, `HasCollectedErrors`, `GetCollectedErrors`, `ClearCollectedErrors`, `ErrorType`, `Internal`, `Client`, `TryFunction`, `GetLastErrorText`, Boolean assignment).
- Resolve changed standalone call targets; when the target declaration has `[TryFunction]`, worklist the ignored-return rule even if the declaration itself is unchanged. Only assignment and conditional use activate try semantics.
A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone.

View file

@ -38,8 +38,9 @@ Discard files that are not applicable. Retain conditionally applicable files (an
Narrow the relevant files to the subset that applies to the changes under review. Exclude test codeunits, test libraries, test helper code, files under test/Test/Tests paths, and objects with `Subtype = Test`; test data is synthetic and does not ship to customers. For each relevant file, compute overlap against: Narrow the relevant files to the subset that applies to the changes under review. Exclude test codeunits, test libraries, test helper code, files under test/Test/Tests paths, and objects with `Subtype = Test`; test data is synthetic and does not ship to customers. For each relevant file, compute overlap against:
- The changed AL object names and types — especially tables and tableextensions (for `DataClassification` on fields), codeunits that call `Error`, `Session.LogMessage`, or `FeatureTelemetry`, codeunits performing outgoing HTTP requests with customer data, migration codeunits, and objects reading or writing `IsolatedStorage`. - The changed AL object names and types — especially tables and tableextensions (for `DataClassification` on fields), codeunits that call `Error`, `Session.LogMessage`, or `FeatureTelemetry`, codeunits performing outgoing HTTP requests with customer data, migration codeunits, and objects reading or writing `IsolatedStorage`.
- The changed procedures and triggers, weighted toward those that call `Error`, `Session.LogMessage`, `StrSubstNo`, `GetLastErrorText`, `FeatureTelemetry.LogUsage`/`LogUptake`/`LogError`, `HttpClient.Post`/`Get`, `IsolatedStorage.Set`/`SetEncrypted`/`Get`, or `PrivacyNotice.GetPrivacyNoticeApprovalState`. - The changed procedures and triggers, weighted toward those that call `Error`, construct `ErrorInfo`, call `Session.LogMessage`, `StrSubstNo`, `GetLastErrorText`/`GetLastErrorCallStack`, `FeatureTelemetry.LogUsage`/`LogUptake`/`LogError`, `HttpClient.Post`/`Get`, `IsolatedStorage.Set`/`SetEncrypted`/`Get`, or `PrivacyNotice.GetPrivacyNoticeApprovalState`.
- Tokens extracted from the diff that relate to privacy (`DataClassification`, `CustomerContent`, `EndUserIdentifiableInformation`, `EndUserPseudonymousIdentifiers`, `SystemMetadata`, `ToBeClassified`, `PrivacyNotice`, `GetLastErrorText`, `TelemetryScope`, `FeatureTelemetry`, `CustomDimensions`, `LogUsage`, `LogUptake`, `LogError`, `HybridSL`, `HybridGP`, `HybridBC`). - Tokens extracted from the diff that relate to privacy (`DataClassification`, `CustomerContent`, `EndUserIdentifiableInformation`, `EndUserPseudonymousIdentifiers`, `SystemMetadata`, `ToBeClassified`, `PrivacyNotice`, `ErrorInfo`, `Message`, `DetailedMessage`, `ErrorType`, `Internal`, `Client`, `GetLastErrorText`, `GetLastErrorCallStack`, `TelemetryScope`, `FeatureTelemetry`, `CustomDimensions`, `LogUsage`, `LogUptake`, `LogError`, `ErrorText`, `ErrorCallStack`, `alErrorText`, `alErrorCallStack`, `HybridSL`, `HybridGP`, `HybridBC`).
- Worklist ErrorInfo privacy guidance when any of `Message`, `DataClassification`, `ErrorType`, or `DetailedMessage` changes. For every `FeatureTelemetry.LogError`, inspect the dedicated error text and call-stack arguments in addition to explicit custom dimensions.
A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone.

View file

@ -38,8 +38,9 @@ Discard files that are not applicable. Retain conditionally applicable files (an
Narrow the relevant files to the subset that applies to the changes under review. For each relevant file, compute overlap against: Narrow the relevant files to the subset that applies to the changes under review. For each relevant file, compute overlap against:
- The changed AL object names and types — especially codeunits with `Subtype = Upgrade` or `Subtype = Install`, tables and tableextensions adding or changing fields, enums and enumextensions, and objects under `Hybrid*`/`Migration`/`Upgrade` namespaces. - The changed AL object names and types — especially codeunits with `Subtype = Upgrade` or `Subtype = Install`, tables and tableextensions adding or changing fields, enums and enumextensions, and objects under `Hybrid*`/`Migration`/`Upgrade` namespaces.
- The changed triggers and procedures, weighted toward `OnUpgradePerCompany`, `OnUpgradePerDatabase`, `OnValidateUpgradePerCompany`, `OnValidateUpgradePerDatabase`, `OnInstallAppPerCompany`, and the `OnGetPerCompanyUpgradeTags`/`OnGetPerDatabaseUpgradeTags` subscribers. - The changed triggers and procedures, weighted toward `OnCheckPreconditionsPerCompany`/`PerDatabase`, `OnUpgradePerCompany`/`PerDatabase`, `OnValidateUpgradePerCompany`/`PerDatabase`, `OnInstallAppPerCompany`/`PerDatabase`, and the `OnGetPerCompanyUpgradeTags`/`OnGetPerDatabaseUpgradeTags` subscribers.
- Tokens extracted from the diff that relate to upgrade concerns (`Subtype = Upgrade`, `Upgrade Tag`, `HasUpgradeTag`, `SetUpgradeTag`, `OnValidateUpgrade`, `DataTransfer`, `CopyFields`, `InitValue`, `ObsoleteState`, `ObsoleteReason`, `ObsoleteTag`, `DataVersion`, `ExecutionContext`, `PrimaryKey`, `key(`, `field(`, `value(`, `enum`, `enumextension`, `HybridSL`, `HybridGP`, `HybridBC`, `HybridBaseDeployment`). - Tokens extracted from the diff that relate to upgrade concerns (`Subtype = Upgrade`, `Subtype = Install`, `Upgrade Tag`, `HasUpgradeTag`, `SetUpgradeTag`, `OnCheckPreconditions`, `OnUpgrade`, `OnValidateUpgrade`, `OnInstallApp`, `DataTransfer`, `CopyFields`, `Insert`, `Modify`, `Delete`, `Rename`, `InitValue`, `ObsoleteState`, `ObsoleteReason`, `ObsoleteTag`, `DataVersion`, `ExecutionContext`, `PrimaryKey`, `key(`, `field(`, `value(`, `enum`, `enumextension`, `HybridSL`, `HybridGP`, `HybridBC`, `HybridBaseDeployment`).
- Worklist the check-only rule when precondition or validation triggers contain writes, and the install-versus-upgrade rule when migration helpers are reachable only from an install codeunit.
A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. When the diff contains no upgrade-related changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files. A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone. When the diff contains no upgrade-related changes by any of the above signals, return `outcome: "not-applicable"` without evaluating files.