Add lifecycle error and privacy knowledge (#99)

* Add lifecycle error and privacy knowledge

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

Copilot-Session: 95c06ad8-377d-4faa-8d07-06300b1c81ec

* Fix lifecycle privacy review findings

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a26cd6d6-ff49-433e-bc53-f645c455ebdd

* Refine lifecycle privacy retrieval

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

Copilot-Session: 95c06ad8-377d-4faa-8d07-06300b1c81ec

* Make review gates explicit

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

Copilot-Session: 95c06ad8-377d-4faa-8d07-06300b1c81ec

---------

Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-07-14 12:52:56 +02:00 committed by GitHub
parent 363f08f47e
commit e0ebdd35c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 459 additions and 39 deletions

View file

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

View file

@ -0,0 +1,15 @@
codeunit 50307 "ErrorInfo Privacy Good"
{
procedure RaiseSynchronizationError()
var
FailureInfo: ErrorInfo;
begin
FailureInfo.Message := SynchronizationFailedErr;
FailureInfo.DataClassification := DataClassification::SystemMetadata;
FailureInfo.ErrorType := ErrorType::Client;
Error(FailureInfo);
end;
var
SynchronizationFailedErr: Label 'The synchronization could not be completed.';
}

View file

@ -0,0 +1,26 @@
---
bc-version: [14..]
domain: privacy
keywords: [errorinfo, errorinfo-message, errorinfo-dataclassification, errorinfo-errortype, errorinfo-detailedmessage, copy-details, telemetry]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Review each ErrorInfo text surface by its actual exposure
## Description
Runtime 3.0 (BC 14) provides `ErrorInfo.Message`, `DataClassification`, and `ErrorType`. `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. Runtime 8.0 (BC 19) adds `DetailedMessage`, which is omitted from the primary message but 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. On BC 19 and later, put only support-safe technical context in `DetailedMessage`, because a user can copy it from the dialog. The samples use only members available at the BC 14 article floor.
See sample: `errorinfo-telemetry-classification-and-errortype.good.al`.
## Anti Pattern
Marking a dynamic customer-bearing `Message` as `SystemMetadata`, or assuming `ErrorType::Internal` keeps it out of telemetry. On BC 19 and later, the same anti-pattern includes placing secrets or 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`.