Telemetry Reference Patterns
Advanced patterns for Application Insights telemetry in Business Central extensions.
Telemetry Wrapper Pattern
A SingleInstance codeunit that centralizes all telemetry calls and adds common context automatically.
Full Wrapper Codeunit
codeunit [ID] "[Prefix] Telemetry Wrapper"
{
Access = Internal;
SingleInstance = true;
var
Telemetry: Codeunit Telemetry;
CommonDimensionsInitialized: Boolean;
ExtensionVersion: Text[50];
// ── Lifecycle ──
procedure LogStart(EventId: Text; Message: Text; CustomDimensions: Dictionary of [Text, Text])
begin
AddCommonDimensions(CustomDimensions);
Telemetry.LogMessage(
EventId, Message,
Verbosity::Normal, DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher, CustomDimensions);
end;
procedure LogEnd(EventId: Text; Message: Text; StartTime: DateTime; CustomDimensions: Dictionary of [Text, Text])
var
DurationMs: Duration;
begin
DurationMs := CurrentDateTime - StartTime;
CustomDimensions.Set('DurationMs', Format(DurationMs));
AddCommonDimensions(CustomDimensions);
Telemetry.LogMessage(
EventId, Message,
Verbosity::Normal, DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher, CustomDimensions);
end;
// ── Errors ──
procedure LogError(EventId: Text; Message: Text; CustomDimensions: Dictionary of [Text, Text])
begin
CustomDimensions.Set('ErrorMessage', GetLastErrorText());
CustomDimensions.Set('ErrorCallStack', GetLastErrorCallStack());
AddCommonDimensions(CustomDimensions);
Telemetry.LogMessage(
EventId, Message,
Verbosity::Error, DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher, CustomDimensions);
end;
procedure LogErrorWithDetails(EventId: Text; Message: Text; ErrorDetails: Text; CustomDimensions: Dictionary of [Text, Text])
begin
CustomDimensions.Set('ErrorDetails', ErrorDetails);
CustomDimensions.Set('ErrorCallStack', GetLastErrorCallStack());
AddCommonDimensions(CustomDimensions);
Telemetry.LogMessage(
EventId, Message,
Verbosity::Error, DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher, CustomDimensions);
end;
// ── Warnings ──
procedure LogWarning(EventId: Text; Message: Text; CustomDimensions: Dictionary of [Text, Text])
begin
AddCommonDimensions(CustomDimensions);
Telemetry.LogMessage(
EventId, Message,
Verbosity::Warning, DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher, CustomDimensions);
end;
// ── Feature Usage ──
procedure LogFeatureUsage(EventId: Text; FeatureArea: Text; FeatureAction: Text; CustomDimensions: Dictionary of [Text, Text])
begin
CustomDimensions.Set('FeatureArea', FeatureArea);
CustomDimensions.Set('FeatureAction', FeatureAction);
AddCommonDimensions(CustomDimensions);
Telemetry.LogMessage(
EventId, FeatureArea + ' - ' + FeatureAction,
Verbosity::Normal, DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher, CustomDimensions);
end;
// ── Performance ──
procedure LogPerformanceWarning(EventId: Text; OperationName: Text; DurationMs: Duration; ThresholdMs: Integer; CustomDimensions: Dictionary of [Text, Text])
begin
if DurationMs <= ThresholdMs then
exit;
CustomDimensions.Set('Operation', OperationName);
CustomDimensions.Set('DurationMs', Format(DurationMs));
CustomDimensions.Set('ThresholdMs', Format(ThresholdMs));
AddCommonDimensions(CustomDimensions);
Telemetry.LogMessage(
EventId,
StrSubstNo('Slow operation detected: %1 (%2 ms, threshold %3 ms)', OperationName, DurationMs, ThresholdMs),
Verbosity::Warning, DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher, CustomDimensions);
end;
// ─ ─ Common Dimensions ──
local procedure AddCommonDimensions(var CustomDimensions: Dictionary of [Text, Text])
var
ModuleInfo: ModuleInfo;
EnvironmentInformation: Codeunit "Environment Information";
begin
if not CommonDimensionsInitialized then
InitCommonDimensions();
CustomDimensions.Set('ExtensionVersion', ExtensionVersion);
CustomDimensions.Set('CompanyName', CompanyName);
if EnvironmentInformation.IsSaaS() then
CustomDimensions.Set('EnvironmentType', 'SaaS')
else
CustomDimensions.Set('EnvironmentType', 'OnPrem');
if EnvironmentInformation.IsProduction() then
CustomDimensions.Set('EnvironmentName', 'Production')
else
CustomDimensions.Set('EnvironmentName', 'Sandbox');
end;
local procedure InitCommonDimensions()
var
ModuleInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(ModuleInfo);
ExtensionVersion := Format(ModuleInfo.AppVersion);
CommonDimensionsInitialized := true;
end;
}
Usage
var
TelemetryWrapper: Codeunit "[Prefix] Telemetry Wrapper";
Dims: Dictionary of [Text, Text];
StartTime: DateTime;
begin
StartTime := CurrentDateTime;
Dims.Add('OrderNo', SalesHeader."No.");
TelemetryWrapper.LogStart('BCS-SALES001', 'Processing order', Dims);
// ... logic ...
Clear(Dims);
Dims.Add('OrderNo', SalesHeader."No.");
TelemetryWrapper.LogEnd('BCS-SALES002', 'Order processed', StartTime, Dims);
end;
Contextual Telemetry
Automatically add environment and version context to all telemetry signals.
Environment Detection
local procedure GetEnvironmentType(): Text
var
EnvironmentInformation: Codeunit "Environment Information";
begin
if EnvironmentInformation.IsSaaS() then begin
if EnvironmentInformation.IsProduction() then
exit('SaaS-Production')
else
exit('SaaS-Sandbox');
end else
exit('OnPrem');
end;
Extension Version
local procedure GetExtensionVersion(): Text
var
ModuleInfo: ModuleInfo;
begin
NavApp.GetCurrentModuleInfo(ModuleInfo);
exit(Format(ModuleInfo.AppVersion));
end;
Anonymized User Context
Never log plain user IDs. Anonymize for aggregation:
local procedure GetAnonymizedUserId(): Text
var
CryptographyManagement: Codeunit "Cryptography Management";
begin
// Hash the user security ID for anonymization
exit(CryptographyManagement.GenerateHash(UserSecurityId(), 2)); // SHA256
end;
Telemetry Constants Pattern
Keep all event IDs and messages as labels for consistency and translatability:
codeunit [ID] "[Prefix] Telemetry Constants"
{
Access = Internal;
var
// ── Event IDs ──
SalesOrderStartedEventIdLbl: Label 'BCS-SALES001', Locked = true;
SalesOrderCompletedEventIdLbl: Label 'BCS-SALES002', Locked = true;
SalesOrderErrorEventIdLbl: Label 'BCS-SALESE001', Locked = true;
SalesOrderValidationWarnEventIdLbl: Label 'BCS-SALESW001', Locked = true;
SalesOrderSlowEventIdLbl: Label 'BCS-SALESP001', Locked = true;
// ── Messages ──
SalesOrderStartedMsgLbl: Label 'Sales order processing started', Locked = true;
SalesOrderCompletedMsgLbl: Label 'Sales order processing completed', Locked = true;
SalesOrderErrorMsgLbl: Label 'Sales order processing failed', Locked = true;
SalesOrderValidationWarnMsgLbl: Label 'Sales order validation warning: %1', Locked = true;
}
Benefits:
- Single source of truth for all event IDs
- Easy to search and audit
Locked = trueprevents translation (event IDs must stay stable)
TryFunction Pattern with Telemetry
Wrap error-prone operations in TryFunctions and log failures:
[TryFunction]
local procedure TryPostOrder(var SalesHeader: Record "Sales Header")
var
SalesPost: Codeunit "Sales-Post";
begin
SalesPost.Run(SalesHeader);
end;
procedure PostOrderWithTelemetry(var SalesHeader: Record "Sales Header"): Boolean
var
BCStelemetry: Codeunit "BCS Sales Telemetry";
CustomDimensions: Dictionary of [Text, Text];
StartTime: DateTime;
begin
StartTime := CurrentDateTime;
if not TryPostOrder(SalesHeader) then begin
AddOrderDimensions(CustomDimensions, SalesHeader);
BCStelemetry.LogError('BCS-SALESE001', 'Order posting failed', CustomDimensions);
exit(false);
end;
Clear(CustomDimensions);
AddOrderDimensions(CustomDimensions, SalesHeader);
BCStelemetry.LogOperationCompleted('BCS-SALES002', 'Order posted', StartTime, CustomDimensions);
exit(true);
end;
Conditional Telemetry Pattern
Enable/disable telemetry per module via setup table:
// In setup table
field(10; "Enable Telemetry"; Boolean)
{
Caption = 'Enable Telemetry';
DataClassification = CustomerContent;
InitValue = true;
}
// In telemetry helper
procedure IsEnabled(): Boolean
var
Setup: Record "[Prefix] Setup";
begin
if not Setup.Get() then
exit(true); // Default to enabled
exit(Setup."Enable Telemetry");
end;
procedure LogStart(EventId: Text; Message: Text; CustomDimensions: Dictionary of [Text, Text])
begin
if not IsEnabled() then
exit;
Telemetry.LogMessage(
EventId, Message,
Verbosity::Normal, DataClassification::SystemMetadata,
TelemetryScope::ExtensionPublisher, CustomDimensions);
end;
Bulk Operation Telemetry
For batch/bulk operations, log summary instead of individual items:
procedure ProcessBatch(var ItemJnlBatch: Record "Item Journal Batch")
var
BCStelemetry: Codeunit "BCS Inventory Telemetry";
CustomDimensions: Dictionary of [Text, Text];
StartTime: DateTime;
SuccessCount: Integer;
ErrorCount: Integer;
begin
StartTime := CurrentDateTime;
// Process items ...
// Increment SuccessCount / ErrorCount per item
CustomDimensions.Add('BatchName', ItemJnlBatch.Name);
CustomDimensions.Add('TotalItems', Format(SuccessCount + ErrorCount));
CustomDimensions.Add('SuccessCount', Format(SuccessCount));
CustomDimensions.Add('ErrorCount', Format(ErrorCount));
BCStelemetry.LogOperationCompleted(
'BCS-INV002', 'Batch processing completed', StartTime, CustomDimensions);
if ErrorCount > 0 then begin
Clear(CustomDimensions);
CustomDimensions.Add('BatchName', ItemJnlBatch.Name);
CustomDimensions.Add('ErrorCount', Format(ErrorCount));
BCStelemetry.LogWarning(
'BCS-INVW001', StrSubstNo('Batch had %1 errors', ErrorCount), CustomDimensions);
end;
end;
Amount Range Helper
Bucket amounts into ranges for analytics without exposing exact figures:
local procedure GetAmountRange(Amount: Decimal): Text
begin
case true of
Amount < 100:
exit('0-100');
Amount < 1000:
exit('100-1K');
Amount < 10000:
exit('1K-10K');
Amount < 100000:
exit('10K-100K');
else
exit('100K+');
end;
end;
Use in dimensions:
CustomDimensions.Add('AmountRange', GetAmountRange(SalesHeader."Amount Including VAT"));
HttpClient Telemetry
For external API integrations, log request/response metadata:
procedure CallExternalAPI(Endpoint: Text): Boolean
var
BCStelemetry: Codeunit "BCS Integration Telemetry";
Client: HttpClient;
Response: HttpResponseMessage;
CustomDimensions: Dictionary of [Text, Text];
StartTime: DateTime;
begin
StartTime := CurrentDateTime;
CustomDimensions.Add('Endpoint', Endpoint);
CustomDimensions.Add('Method', 'GET');
BCStelemetry.LogOperationStarted('BCS-API001', 'External API call started', CustomDimensions);
if not Client.Get(Endpoint, Response) then begin
Clear(CustomDimensions);
CustomDimensions.Add('Endpoint', Endpoint);
BCStelemetry.LogError('BCS-APIE001', 'External API call failed', CustomDimensions);
exit(false);
end;
Clear(CustomDimensions);
CustomDimensions.Add('Endpoint', Endpoint);
CustomDimensions.Add('StatusCode', Format(Response.HttpStatusCode));
CustomDimensions.Add('IsSuccess', Format(Response.IsSuccessStatusCode));
BCStelemetry.LogOperationCompleted('BCS-API002', 'External API call completed', StartTime, CustomDimensions);
exit(Response.IsSuccessStatusCode);
end;
Testing Telemetry
Verify telemetry is emitted correctly in test codeunits:
[Test]
procedure TestTelemetryEmittedOnOrderProcessing()
var
SalesHeader: Record "Sales Header";
SalesOrderProcessor: Codeunit "Sales Order Processor";
begin
// [GIVEN] A valid sales order
CreateSalesOrder(SalesHeader);
// [WHEN] Processing the order
SalesOrderProcessor.ProcessOrder(SalesHeader);
// [THEN] No error occurs and the operation completes
// Telemetry verification is done via Application Insights,
// not in unit tests. Here we just verify no runtime errors.
end;
Note: AL does not provide a built-in way to assert telemetry emissions in test codeunits. Verify telemetry in Application Insights using KQL queries after deployment.