mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-10-05 14:46:55 +01:00
Address review feedback on retention policy knowledge
- Scope both articles to bc-version [17..] (retention policies shipped in v17). - Allowed-tables sample: add OnRefreshAllowedTables subscriber with a ForceUpdate path; the upgrade tag now gates one-time setup only. - Default-policy sample: use Retention Policy Setup.FindOrCreateRetentionPeriod instead of a hand-rolled lookup-then-insert that can collide on code. - Anti-pattern now keys on append-only tables rather than table names. - al-privacy-review: add retention-policy tokens and deterministic routing for both articles, with a bounded per-table text search for delete and registration paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2f3e5afac1
commit
4bef582ffe
5 changed files with 37 additions and 30 deletions
|
|
@ -3,12 +3,21 @@ codeunit 50560 "Contoso Reten. Pol. Setup"
|
|||
Access = Internal;
|
||||
|
||||
procedure AddAllowedTables()
|
||||
begin
|
||||
AddAllowedTables(false);
|
||||
end;
|
||||
|
||||
// ForceUpdate re-registers even after the upgrade tag is set, so the
|
||||
// table comes back when an administrator refreshes the allowed tables.
|
||||
procedure AddAllowedTables(ForceUpdate: Boolean)
|
||||
var
|
||||
ContosoActivityLog: Record "Contoso Activity Log";
|
||||
RetenPolAllowedTables: Codeunit "Reten. Pol. Allowed Tables";
|
||||
UpgradeTag: Codeunit "Upgrade Tag";
|
||||
IsInitialSetup: Boolean;
|
||||
begin
|
||||
if UpgradeTag.HasUpgradeTag(AllowedTableTag()) then
|
||||
IsInitialSetup := not UpgradeTag.HasUpgradeTag(AllowedTableTag());
|
||||
if not (IsInitialSetup or ForceUpdate) then
|
||||
exit;
|
||||
|
||||
if not RetenPolAllowedTables.IsAllowedTable(Database::"Contoso Activity Log") then
|
||||
|
|
@ -17,13 +26,20 @@ codeunit 50560 "Contoso Reten. Pol. Setup"
|
|||
ContosoActivityLog.FieldNo(SystemCreatedAt),
|
||||
28); // support cases need at least four weeks of log history
|
||||
|
||||
UpgradeTag.SetUpgradeTag(AllowedTableTag());
|
||||
if IsInitialSetup then
|
||||
UpgradeTag.SetUpgradeTag(AllowedTableTag());
|
||||
end;
|
||||
|
||||
local procedure AllowedTableTag(): Code[250]
|
||||
begin
|
||||
exit('Contoso-ActivityLogAllowedTable-20260910');
|
||||
end;
|
||||
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Reten. Pol. Allowed Tables", OnRefreshAllowedTables, '', false, false)]
|
||||
local procedure AddAllowedTablesOnRefreshAllowedTables()
|
||||
begin
|
||||
AddAllowedTables(true);
|
||||
end;
|
||||
}
|
||||
|
||||
codeunit 50561 "Contoso Reten. Pol. Install"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
bc-version: [17..]
|
||||
domain: privacy
|
||||
keywords: [retention-policy, allowed-tables, addallowedtable, reten-pol-allowed-tables, log-table-growth, mandatory-minimum-retention, install-upgrade-codeunit]
|
||||
keywords: [retention-policy, allowed-tables, addallowedtable, reten-pol-allowed-tables, onrefreshallowedtables, append-only-table, log-table-growth, deleteall, mandatory-minimum-retention, install-upgrade-codeunit]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
|
|
@ -15,7 +15,7 @@ The retention policy engine only ever deletes from tables that appear in its all
|
|||
|
||||
## Best Practice
|
||||
|
||||
Register every table the extension owns that accumulates rows over time: activity and audit logs, integration and API request logs, archived documents. Call a shared routine from both the install codeunit (`OnInstallAppPerCompany`) and an upgrade codeunit (`OnUpgradePerCompany`), because install code does not run when an existing installation moves to a new version — registration added only to install code never reaches tenants that already have the app. Guard the routine with `IsAllowedTable` and an upgrade tag so repeated runs are idempotent. Pass `MandatoryMinRetenDays` when the data must survive a minimum period for audit or support reasons; the platform then rejects any shorter period an administrator configures. When only a subset of rows should ever expire, build the filter with `AddTableFilterToJsonArray` and pass it to the `AddAllowedTable` overload that takes a `JsonArray` — a filter added as locked cannot be removed later by the administrator.
|
||||
Register every table the extension owns that accumulates rows over time: activity and audit logs, integration and API request logs, archived documents. Call a shared routine from both the install codeunit (`OnInstallAppPerCompany`) and an upgrade codeunit (`OnUpgradePerCompany`), because install code does not run when an existing installation moves to a new version — registration added only to install code never reaches tenants that already have the app. Guard the routine with `IsAllowedTable` and an upgrade tag so repeated runs are idempotent, but keep a force path past the tag: the **Retention Policies** pages raise `Reten. Pol. Allowed Tables.OnRefreshAllowedTables`, and the platform's own installers (System Application, Base Application, Shopify) subscribe to it and re-run registration with `ForceUpdate`. A routine that exits whenever the tag is set cannot take part in that refresh, so the upgrade tag should gate one-time setup only, not re-registration. Pass `MandatoryMinRetenDays` when the data must survive a minimum period for audit or support reasons; the platform then rejects any shorter period an administrator configures. When only a subset of rows should ever expire, build the filter with `AddTableFilterToJsonArray` and pass it to the `AddAllowedTable` overload that takes a `JsonArray` — a filter added as locked cannot be removed later by the administrator.
|
||||
|
||||
Registering a table only makes it eligible; the policy itself is a separate concern, covered by [`ship-a-default-retention-policy-setup.md`](ship-a-default-retention-policy-setup.md).
|
||||
|
||||
|
|
@ -23,11 +23,11 @@ See sample: [`register-owned-log-tables-for-retention-policies.good.al`](registe
|
|||
|
||||
## Anti Pattern
|
||||
|
||||
An extension-owned log table with no `AddAllowedTable` call anywhere in the app, cleaned instead by hand-rolled code — a job queue codeunit or scheduled task running `DeleteAll` against a hard-coded date window. The deletion happens outside the **Retention Policy Log**, the administrator has no page on which to lengthen, shorten, or disable it, and the table is invisible during a data-retention review. The same defect in slower form is registration performed only in the install codeunit: new tenants are covered, every existing tenant stays unregistered after the upgrade.
|
||||
An extension-owned table that only grows — the app inserts into it but never deletes from it — with no `AddAllowedTable` call anywhere in the app. The name is not a reliable signal: an "Incoming Data" buffer-history table grows the same way an "Activity Log" does, and such tables have reached hundreds of gigabytes on customer tenants. A variant is a table cleaned by hand-rolled code — a job queue codeunit or scheduled task running `DeleteAll` against a hard-coded date window: the deletion happens outside the **Retention Policy Log**, the administrator has no page on which to lengthen, shorten, or disable it, and the table is invisible during a data-retention review. The same defect in slower form is registration performed only in the install codeunit: new tenants are covered, every existing tenant stays unregistered after the upgrade.
|
||||
|
||||
See sample: [`register-owned-log-tables-for-retention-policies.bad.al`](register-owned-log-tables-for-retention-policies.bad.al).
|
||||
|
||||
## References
|
||||
|
||||
- [Clean up data with retention policies](https://learn.microsoft.com/en-us/dynamics365/business-central/admin-data-retention-policies), section *Include your extension in a retention policy*.
|
||||
- [`RetenPolAllowedTables.Codeunit.al`](https://github.com/microsoft/BCApps/blob/main/src/System%20Application/App/Retention%20Policy/src/Retention%20Policy%20Allowed%20Tables/RetenPolAllowedTables.Codeunit.al) in microsoft/BCApps — the `AddAllowedTable` overloads, `IsAllowedTable`, and `AddTableFilterToJsonArray`.
|
||||
- [`RetenPolAllowedTables.Codeunit.al`](https://github.com/microsoft/BCApps/blob/main/src/System%20Application/App/Retention%20Policy/src/Retention%20Policy%20Allowed%20Tables/RetenPolAllowedTables.Codeunit.al) in microsoft/BCApps — the `AddAllowedTable` overloads, `IsAllowedTable`, `AddTableFilterToJsonArray`, and `OnRefreshAllowedTables`.
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ codeunit 50564 "Contoso Reten. Pol. Default"
|
|||
{
|
||||
Access = Internal;
|
||||
|
||||
var
|
||||
SixMonthsTok: Label 'Six Months', MaxLength = 20;
|
||||
|
||||
procedure CreateDefaultPolicy()
|
||||
var
|
||||
RetentionPolicySetup: Record "Retention Policy Setup";
|
||||
RetentionPolicySetupMgt: Codeunit "Retention Policy Setup";
|
||||
UpgradeTag: Codeunit "Upgrade Tag";
|
||||
begin
|
||||
// Created once per company: an administrator who deletes the policy
|
||||
|
|
@ -18,7 +16,9 @@ codeunit 50564 "Contoso Reten. Pol. Default"
|
|||
if not RetentionPolicySetup.Get(Database::"Contoso Activity Log") then begin
|
||||
RetentionPolicySetup.Validate("Table Id", Database::"Contoso Activity Log");
|
||||
RetentionPolicySetup.Validate("Apply to all records", true);
|
||||
RetentionPolicySetup.Validate("Retention Period", SixMonthRetentionPeriod());
|
||||
RetentionPolicySetup.Validate(
|
||||
"Retention Period",
|
||||
RetentionPolicySetupMgt.FindOrCreateRetentionPeriod("Retention Period Enum"::"6 Months"));
|
||||
RetentionPolicySetup.Validate(Enabled, false); // the administrator opts in to deletion
|
||||
RetentionPolicySetup.Insert(true);
|
||||
end;
|
||||
|
|
@ -26,21 +26,6 @@ codeunit 50564 "Contoso Reten. Pol. Default"
|
|||
UpgradeTag.SetUpgradeTag(DefaultPolicyTag());
|
||||
end;
|
||||
|
||||
local procedure SixMonthRetentionPeriod(): Code[20]
|
||||
var
|
||||
RetentionPeriod: Record "Retention Period";
|
||||
begin
|
||||
RetentionPeriod.SetRange("Retention Period", RetentionPeriod."Retention Period"::"6 Months");
|
||||
if RetentionPeriod.FindFirst() then
|
||||
exit(RetentionPeriod.Code);
|
||||
|
||||
RetentionPeriod.Code := CopyStr(UpperCase(SixMonthsTok), 1, MaxStrLen(RetentionPeriod.Code));
|
||||
RetentionPeriod.Description := SixMonthsTok;
|
||||
RetentionPeriod.Validate("Retention Period", RetentionPeriod."Retention Period"::"6 Months");
|
||||
RetentionPeriod.Insert(true);
|
||||
exit(RetentionPeriod.Code);
|
||||
end;
|
||||
|
||||
local procedure DefaultPolicyTag(): Code[250]
|
||||
begin
|
||||
exit('Contoso-ActivityLogDefaultPolicy-20260910');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
bc-version: [17..]
|
||||
domain: privacy
|
||||
keywords: [retention-policy-setup, retention-period, default-policy, unbounded-table-growth, opt-in-deletion, upgrade-tag]
|
||||
keywords: [retention-policy, retention-policy-setup, addallowedtable, findorcreateretentionperiod, retention-period, default-policy, unbounded-table-growth, opt-in-deletion, upgrade-tag]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
|
|
@ -15,7 +15,7 @@ application-area: [all]
|
|||
|
||||
## Best Practice
|
||||
|
||||
In the same install and upgrade routine that registers the table (see [`register-owned-log-tables-for-retention-policies.md`](register-owned-log-tables-for-retention-policies.md)), create the `Retention Policy Setup` record: reuse an existing `Retention Period` whose `"Retention Period"` enum value matches the period you want and create one only when none exists, then `Validate` `"Table Id"`, `"Apply to all records"` and `"Retention Period"` before inserting. Gate the creation on an upgrade tag so it happens once per company rather than on every upgrade. Default to inserting with `Enabled` set to false: pre-creating the line puts a reviewed, sensible period in front of the administrator while leaving the decision to delete tenant data with them. Shipping the policy enabled is defensible for rows that are purely diagnostic and documented as transient — state that choice, and the default period, in the app's onboarding material either way.
|
||||
In the same install and upgrade routine that registers the table (see [`register-owned-log-tables-for-retention-policies.md`](register-owned-log-tables-for-retention-policies.md)), create the `Retention Policy Setup` record: get the period code from `Codeunit "Retention Policy Setup".FindOrCreateRetentionPeriod`, which reuses an existing `Retention Period` with the requested enum value and otherwise creates one without colliding on an existing code (a hand-written lookup-then-insert fails when a period with the chosen code already exists for a different value), then `Validate` `"Table Id"`, `"Apply to all records"` and `"Retention Period"` before inserting. Gate the creation on an upgrade tag so it happens once per company rather than on every upgrade. Default to inserting with `Enabled` set to false: pre-creating the line puts a reviewed, sensible period in front of the administrator while leaving the decision to delete tenant data with them. Shipping the policy enabled is defensible for rows that are purely diagnostic and documented as transient — state that choice, and the default period, in the app's onboarding material either way.
|
||||
|
||||
See sample: [`ship-a-default-retention-policy-setup.good.al`](ship-a-default-retention-policy-setup.good.al).
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue