Complete AL review knowledge readiness (#108)
Some checks failed
Validate knowledge index / validate-index (push) Has been cancelled
Validate AL review fixtures / validate-review-fixtures (push) Has been cancelled
Validate frontmatter and structure / validate (push) Has been cancelled

* Complete AL review knowledge readiness

Fill telemetry and Query coverage, strengthen thin review domains, correct audited content defects, and add deterministic cheap-model evaluation and reference-integrity safeguards.

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

Copilot-Session: 9825b012-e653-496a-9310-c1f4b6f8ac27

* Generalize review fixture discovery

Derive smoke cases from the leaf, domain, and paired-sample conventions so new leaves require no scoring-contract changes. Keep only exceptional selection/context overrides and fail when retrieval metadata cannot rank the selected article.

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

Copilot-Session: 9825b012-e653-496a-9310-c1f4b6f8ac27

* Preserve published field IDs in sample

Keep the existing Email and Contact Email field IDs unchanged, clarify that the sample represents an independent baseline, and use a local breaking-change rule for the generic smoke evaluation.

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

Copilot-Session: 9825b012-e653-496a-9310-c1f4b6f8ac27

* Clarify published field identity rules

State explicitly that a published field keeps its ID, name, and type while a replacement is added as a separate field under an unused ID.

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

Copilot-Session: 9825b012-e653-496a-9310-c1f4b6f8ac27

* Align field obsoletion sample baselines

Use Email field ID 3 as the shared baseline so the bad example demonstrates a same-ID rename while the good example retains the original field and adds a separate replacement.

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

Copilot-Session: 9825b012-e653-496a-9310-c1f4b6f8ac27

---------

Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-07-15 10:55:25 +02:00 committed by GitHub
parent ae04938c03
commit 186d8a1314
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
105 changed files with 2229 additions and 212 deletions

View file

@ -0,0 +1,26 @@
codeunit 50401 "Telemetry Scope Bad"
{
procedure LogIntegrationFailure()
begin
// Tenant operators cannot see an actionable integration failure.
Session.LogMessage(
'TLM0004',
'Document exchange failed',
Verbosity::Error,
DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher,
'Operation', 'DocumentExchange');
end;
procedure LogCacheMiss()
begin
// Environment telemetry receives publisher-only implementation noise.
Session.LogMessage(
'TLM0005',
'Internal cache entry missed',
Verbosity::Verbose,
DataClassification::SystemMetadata,
TelemetryScope::All,
'Cache', 'ExchangeMetadata');
end;
}

View file

@ -0,0 +1,24 @@
codeunit 50400 "Telemetry Scope Good"
{
procedure LogIntegrationFailure()
begin
Session.LogMessage(
'TLM0002',
'Document exchange failed',
Verbosity::Error,
DataClassification::SystemMetadata,
TelemetryScope::All,
'Operation', 'DocumentExchange');
end;
procedure LogCacheMiss()
begin
Session.LogMessage(
'TLM0003',
'Internal cache entry missed',
Verbosity::Verbose,
DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher,
'Cache', 'ExchangeMetadata');
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [17..]
domain: telemetry
keywords: [telemetryscope, extensionpublisher, all, audience, logmessage, application-insights]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Choose TelemetryScope by who must receive the signal
## Description
`TelemetryScope::ExtensionPublisher` sends a custom trace only to the Application Insights resource configured by the extension publisher. `TelemetryScope::All` also sends it to the environment's telemetry, where the customer or partner operating the tenant can query it. The compiler accepts either value, so a plausible-looking scope can silently hide an actionable signal from tenant operators or expose publisher-only implementation noise to them.
## Best Practice
Use `ExtensionPublisher` for internal diagnostics that only the app publisher can interpret, such as cache behavior or private algorithm state. Use `All` for signals the tenant operator can act on, such as an integration failure, quota warning, or setup problem. Decide the audience independently from `DataClassification`; privacy guidance still governs whether the payload may be emitted at all.
See sample: `choose-telemetry-scope-by-audience.good.al`.
## Anti Pattern
Defaulting every call to `All`, including low-level implementation diagnostics, or defaulting every call to `ExtensionPublisher` and thereby hiding customer-actionable failures from environment telemetry. Review only when the message and surrounding branch make the intended audience clear; an ambiguous diagnostic is not enough to infer the wrong scope.
See sample: `choose-telemetry-scope-by-audience.bad.al`.

View file

@ -0,0 +1,11 @@
codeunit 50405 "Feature Uptake Bad"
{
procedure FeatureOpened()
var
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
// The first uptake state skips Discovered and is not emitted.
FeatureTelemetry.LogUptake(
'TLM0011', 'Document exchange', Enum::"Feature Uptake Status"::Used);
end;
}

View file

@ -0,0 +1,26 @@
codeunit 50404 "Feature Uptake Good"
{
procedure FeatureDiscovered()
var
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
FeatureTelemetry.LogUptake(
'TLM0008', 'Document exchange', Enum::"Feature Uptake Status"::Discovered);
end;
procedure FeatureSetUp()
var
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
FeatureTelemetry.LogUptake(
'TLM0009', 'Document exchange', Enum::"Feature Uptake Status"::"Set up");
end;
procedure FeatureUsed()
var
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
FeatureTelemetry.LogUptake(
'TLM0010', 'Document exchange', Enum::"Feature Uptake Status"::Used);
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [18..]
domain: telemetry
keywords: [featuretelemetry, loguptake, discovered, set-up, used, uptake-status, lifecycle]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Emit FeatureTelemetry uptake states in lifecycle order
## Description
`FeatureTelemetry.LogUptake` accepts `Discovered`, `Set up`, `Used`, and `Undiscovered`, but the platform records the forward transition only as `Discovered` to `Set up` to `Used`. If the first call for a feature is `Set up` or `Used`, no uptake telemetry is emitted. `Undiscovered` is the explicit reset from any state.
## Best Practice
Log `Discovered` when the user encounters the feature, `Set up` after its setup is completed, and `Used` when the user attempts it. Keep the same feature name throughout the funnel. Review ordering only when the changed repository context shows the feature's lifecycle; a single isolated `Used` call cannot prove that earlier states are absent elsewhere.
See sample: `feature-uptake-transitions-in-order.good.al`.
## Anti Pattern
Introducing a feature whose only uptake call jumps directly to `Set up` or `Used`, or using different feature-name literals for successive states. The calls compile and run, but the funnel silently omits the invalid transition.
See sample: `feature-uptake-transitions-in-order.bad.al`.

View file

@ -0,0 +1,23 @@
codeunit 50407 "Feature Usage Bad"
{
procedure ExchangeDocument(ShouldFail: Boolean)
var
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
FeatureTelemetry.LogUsage(
'TLM0014', 'Document exchange', 'Document exchanged');
if not TryExchangeDocument(ShouldFail) then
exit;
end;
[TryFunction]
local procedure TryExchangeDocument(ShouldFail: Boolean)
begin
if ShouldFail then
Error(ExchangeFailedErr);
end;
var
ExchangeFailedErr: Label 'Exchange failed.';
}

View file

@ -0,0 +1,27 @@
codeunit 50406 "Feature Usage Good"
{
procedure ExchangeDocument(ShouldFail: Boolean)
var
FeatureTelemetry: Codeunit "Feature Telemetry";
begin
if not TryExchangeDocument(ShouldFail) then begin
FeatureTelemetry.LogError(
'TLM0012', 'Document exchange', 'Exchanging document',
GetLastErrorText(true), GetLastErrorCallStack());
exit;
end;
FeatureTelemetry.LogUsage(
'TLM0013', 'Document exchange', 'Document exchanged');
end;
[TryFunction]
local procedure TryExchangeDocument(ShouldFail: Boolean)
begin
if ShouldFail then
Error(ExchangeFailedErr);
end;
var
ExchangeFailedErr: Label 'Exchange failed.';
}

View file

@ -0,0 +1,26 @@
---
bc-version: [18..]
domain: telemetry
keywords: [featuretelemetry, logusage, logerror, success, tryfunction, feature-usage]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Call FeatureTelemetry.LogUsage only after successful use
## Description
`FeatureTelemetry.LogUsage` means that a user successfully used the feature. An attempt belongs in the uptake funnel, while a failed operation belongs in `LogError`. Logging usage before checking the result inflates adoption metrics with failed attempts and makes usage telemetry disagree with the actual business outcome.
## Best Practice
Call `LogUsage` only after the operation has completed successfully. On a failure path, call `LogError` with the captured error text and call stack when the failure must be emitted explicitly. Use a past-tense event name for usage and a present-tense scenario name for errors.
See sample: `feature-usage-only-after-success.good.al`.
## Anti Pattern
Calling `LogUsage` before a Boolean result, `TryFunction`, `Codeunit.Run`, or HTTP status has been checked, or calling it in both success and failure branches. Do not flag an attempt recorded with `LogUptake(...Used)`; unlike `LogUsage`, that state intentionally records an attempt.
See sample: `feature-usage-only-after-success.bad.al`.

View file

@ -0,0 +1,14 @@
codeunit 50412 "Telemetry Dimension Bad"
{
procedure LogBatchResult(RecordCount: Integer)
var
CustomDimensions: Dictionary of [Text, Text];
begin
CustomDimensions.Add('record count', Format(RecordCount));
CustomDimensions.Add('result_code', 'Success');
Session.LogMessage(
'TLM0015', 'Order processing completed', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
}

View file

@ -0,0 +1,14 @@
codeunit 50411 "Telemetry Dimension Good"
{
procedure LogBatchResult(RecordCount: Integer)
var
CustomDimensions: Dictionary of [Text, Text];
begin
CustomDimensions.Add('RecordCount', Format(RecordCount));
CustomDimensions.Add('Result', 'Success');
Session.LogMessage(
'TLM0015', 'Order processing completed', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [17..]
domain: telemetry
keywords: [customdimensions, dimension-key, schema, pascalcase, kql, breaking-change]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Treat custom dimension keys as a stable telemetry schema
## Description
Business Central prefixes AL custom-dimension keys with `al` in Application Insights, so an AL key named `Result` becomes `alResult`. Microsoft guidance treats telemetry definitions as an API: changing or removing a custom dimension can break dashboards and alerts. PascalCase keys without spaces also compose cleanly in KQL; spaces force awkward bracket access and make queries harder to maintain.
## Best Practice
Choose stable PascalCase keys such as `Operation`, `Result`, and `RecordCount`. Keep the key set and meaning stable for a shipped event ID; add a new event ID or coordinate a schema migration when the meaning must change. Privacy guidance separately governs whether a dimension value may contain customer data.
See sample: `keep-custom-dimension-schema-stable.good.al`.
## Anti Pattern
Keys such as `'order no'` or `'result_code'`, or renaming/removing a key while retaining the same shipped event ID. A naming-only issue is advisory; changing an existing event's schema is the material compatibility defect. New keys on a new event ID are not a breaking change.
See sample: `keep-custom-dimension-schema-stable.bad.al`.

View file

@ -0,0 +1,24 @@
codeunit 50403 "Telemetry Verbosity Bad"
{
procedure RunExchange()
begin
if TryExchange() then
exit;
Session.LogMessage(
'TLM0007',
'Document exchange failed',
Verbosity::Normal,
DataClassification::SystemMetadata,
TelemetryScope::All);
end;
[TryFunction]
local procedure TryExchange()
begin
Error(ExchangeFailedErr);
end;
var
ExchangeFailedErr: Label 'Exchange failed.';
}

View file

@ -0,0 +1,24 @@
codeunit 50402 "Telemetry Verbosity Good"
{
procedure RunExchange()
begin
if TryExchange() then
exit;
Session.LogMessage(
'TLM0006',
'Document exchange failed',
Verbosity::Error,
DataClassification::SystemMetadata,
TelemetryScope::All);
end;
[TryFunction]
local procedure TryExchange()
begin
Error(ExchangeFailedErr);
end;
var
ExchangeFailedErr: Label 'Exchange failed.';
}

View file

@ -0,0 +1,26 @@
---
bc-version: [17..]
domain: telemetry
keywords: [verbosity, severitylevel, critical, error, warning, normal, verbose, logmessage]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Match telemetry Verbosity to the signal's actual severity
## Description
`Verbosity` becomes the Application Insights `severityLevel` and participates in on-premises diagnostic trace filtering. `Critical` represents abnormal termination, `Error` a severe error, `Warning` a warning, `Normal` a non-error event, and `Verbose` detailed tracing. Logging a caught failure as `Normal` is not cosmetic: severity-based alerts miss it, and an on-premises service configured to emit only warnings and above can drop it completely.
## Best Practice
Use `Error` for failed operations that need investigation and `Critical` only for abnormal termination or equivalent loss of service. Use `Warning` for degraded but completed behavior, `Normal` for successful business events, and `Verbose` for detailed diagnostics. Judge the outcome, not the procedure name: an expected optional lookup miss can legitimately remain `Normal` or `Verbose`.
See sample: `match-verbosity-to-signal-severity.good.al`.
## Anti Pattern
A `Session.LogMessage` in a failed `TryFunction`, failed `Codeunit.Run`, unsuccessful HTTP response, or other explicit failure branch that uses `Verbosity::Normal` or `Verbose` without evidence that the failure is expected and benign.
See sample: `match-verbosity-to-signal-severity.bad.al`.

View file

@ -0,0 +1,37 @@
codeunit 50409 "First Telemetry Logger" implements "Telemetry Logger"
{
Access = Internal;
procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text])
begin
Session.LogMessage(
EventId, Message, Verbosity, DataClassification, TelemetryScope, CustomDimensions);
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Telemetry Loggers", 'OnRegisterTelemetryLogger', '', true, true)]
local procedure RegisterFirst(var Sender: Codeunit "Telemetry Loggers")
var
Logger: Codeunit "First Telemetry Logger";
begin
Sender.Register(Logger);
end;
}
codeunit 50410 "Second Telemetry Logger" implements "Telemetry Logger"
{
Access = Internal;
procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text])
begin
Session.LogMessage(
EventId, Message, Verbosity, DataClassification, TelemetryScope, CustomDimensions);
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Telemetry Loggers", 'OnRegisterTelemetryLogger', '', true, true)]
local procedure RegisterSecond(var Sender: Codeunit "Telemetry Loggers")
var
Logger: Codeunit "Second Telemetry Logger";
begin
Sender.Register(Logger);
end;
}

View file

@ -0,0 +1,18 @@
codeunit 50408 "Sample Telemetry Logger" implements "Telemetry Logger"
{
Access = Internal;
procedure LogMessage(EventId: Text; Message: Text; Verbosity: Verbosity; DataClassification: DataClassification; TelemetryScope: TelemetryScope; CustomDimensions: Dictionary of [Text, Text])
begin
Session.LogMessage(
EventId, Message, Verbosity, DataClassification, TelemetryScope, CustomDimensions);
end;
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Telemetry Loggers", 'OnRegisterTelemetryLogger', '', true, true)]
local procedure OnRegisterTelemetryLogger(var Sender: Codeunit "Telemetry Loggers")
var
SampleTelemetryLogger: Codeunit "Sample Telemetry Logger";
begin
Sender.Register(SampleTelemetryLogger);
end;
}

View file

@ -0,0 +1,26 @@
---
bc-version: [18..]
domain: telemetry
keywords: [telemetry-logger, interface, register, publisher, featuretelemetry, onregistertelemetrylogger]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Register exactly one Telemetry Logger implementation per publisher
## Description
The `Telemetry` and `Feature Telemetry` codeunits reach an extension publisher's telemetry through an implementation of the `"Telemetry Logger"` interface registered with `"Telemetry Loggers".OnRegisterTelemetryLogger`. The platform requires exactly one registration per app publisher. No registration prevents the module from working as expected; multiple registrations make the destination ambiguous and produce platform error telemetry.
## Best Practice
Place one internal logger implementation in one app for the publisher, forward its `LogMessage` method to `Session.LogMessage`, and register it from one event subscriber. Companion apps with the same publisher reuse that registration instead of each adding another. Evaluate absence only with repository or app-family context; a single-file diff cannot prove that no logger exists elsewhere.
See sample: `register-one-telemetry-logger-per-publisher.good.al`.
## Anti Pattern
Adding `FeatureTelemetry` calls to a complete app with no logger registration, or registering two logger implementations for apps that share the same publisher. The calls compile, but the telemetry module reports the missing or duplicate registration instead of behaving as intended.
See sample: `register-one-telemetry-logger-per-publisher.bad.al`.

View file

@ -0,0 +1,13 @@
codeunit 50260 "Telemetry Event Id Bad"
{
procedure LogCustomerProcessed(var Customer: Record Customer)
begin
Session.LogMessage(
'0000',
'Customer record processed',
Verbosity::Normal,
DataClassification::SystemMetadata,
TelemetryScope::All,
'Category', 'QualitySamples');
end;
}

View file

@ -0,0 +1,13 @@
codeunit 50261 "Telemetry Event Id Good"
{
procedure LogCustomerProcessed(var Customer: Record Customer)
begin
Session.LogMessage(
'QS0001',
'Customer record processed',
Verbosity::Normal,
DataClassification::SystemMetadata,
TelemetryScope::All,
'Category', 'QualitySamples');
end;
}

View file

@ -0,0 +1,32 @@
---
bc-version: [17..]
domain: telemetry
keywords: [telemetry, logmessage, event-id, sessionlogmessage, observability]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Telemetry event IDs must be stable, unique, and non-placeholder
## Description
The first parameter of `Session.LogMessage` is the **event ID**. Telemetry consumers — Application Insights queries, KQL dashboards, alert rules, support runbooks — pivot on this ID to filter and aggregate events. The contract works only when the ID is:
- **Stable** across releases: the same logical event keeps the same ID, so existing queries continue to match it.
- **Unique** within the extension's telemetry catalogue: two different events MUST NOT share an ID, or downstream consumers cannot distinguish them.
- **Non-placeholder**: literal IDs like `'0000'`, `'1234'`, `'TODO'`, or `'XX0000'` are placeholders that collide with other placeholder-using extensions, are unsearchable, and indicate the catalogue entry was never registered.
The convention used by Microsoft first-party AL code is a short prefix identifying the publisher or feature followed by a numeric suffix — for example `'AL0001'`, `'CUST0042'`, `'SHPFY-0007'`. The exact format is up to the extension; the requirements are stability, uniqueness, and that the chosen ID is registered in whatever catalogue or wiki the extension's telemetry consumers reference.
## Best Practice
Assign each `Session.LogMessage` call a real, registered event ID drawn from the extension's catalogue. Treat the ID as part of the public contract of the event — renaming it is a breaking change for consumers. Keep IDs short, deterministic, and free of personal or environment-specific tokens.
See sample: `telemetry-event-id-stable-unique.good.al`.
## Anti Pattern
Calling `Session.LogMessage('0000', ...)` (or `'1234'`, `'TODO'`, an empty string, a GUID generated at runtime, or any other placeholder) leaves the event unsearchable and indistinguishable from every other event using the same placeholder. The catalogue entry never gets created because the developer "will fix it later", and the placeholder ships.
See sample: `telemetry-event-id-stable-unique.bad.al`.