bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
516 lines (443 loc) • 20.1 kB
Markdown
# Custom Telemetry Implementation - AL Code Examples
## Basic Session.LogMessage() Implementation
```al
// Basic telemetry logging with Session.LogMessage()
codeunit 50300 "Basic Telemetry Examples"
{
procedure LogBasicEvent()
var
CustomDimensions: Dictionary of [Text, Text];
begin
// Simple event logging without custom dimensions
Session.LogMessage('0001', 'User accessed customer list', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, '');
// Event logging with basic custom dimensions
CustomDimensions.Add('FeatureName', 'CustomerManagement');
CustomDimensions.Add('ActionType', 'ViewList');
Session.LogMessage('0002', 'Customer list accessed with filters', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
}
```
## Privacy-Compliant Telemetry Patterns
```al
// Privacy-compliant telemetry implementation
codeunit 50301 "Privacy Safe Telemetry"
{
procedure LogUserActionSafely(CustomerNo: Code[20]; ActionPerformed: Text)
var
CustomDimensions: Dictionary of [Text, Text];
HashedCustomerNo: Text;
begin
// CORRECT: Hash or anonymize sensitive data
HashedCustomerNo := CreateGuid(); // Simplified example - use proper hashing in production
CustomDimensions.Add('CustomerHash', HashedCustomerNo);
CustomDimensions.Add('ActionType', ActionPerformed);
CustomDimensions.Add('CompanySize', GetCompanySizeCategory());
CustomDimensions.Add('UserRole', GetUserRoleCategory());
Session.LogMessage('0010', 'Customer action performed', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
procedure LogSalesDocumentEvent(DocumentType: Enum "Sales Document Type"; LineCount: Integer)
var
CustomDimensions: Dictionary of [Text, Text];
begin
// CORRECT: Log aggregated, non-sensitive data
CustomDimensions.Add('DocumentType', Format(DocumentType));
CustomDimensions.Add('LineCountRange', GetLineCountRange(LineCount));
CustomDimensions.Add('ProcessingTime', Format(CurrentDateTime - StartTime));
Session.LogMessage('0011', 'Sales document processed', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
local procedure GetCompanySizeCategory(): Text
var
CustomerCount: Integer;
Customer: Record Customer;
begin
// Categorize without exposing exact counts
CustomerCount := Customer.Count();
case CustomerCount of
0..50: exit('Small');
51..500: exit('Medium');
else exit('Large');
end;
end;
local procedure GetLineCountRange(LineCount: Integer): Text
begin
// Return ranges instead of exact counts for privacy
case LineCount of
0..5: exit('1-5');
6..20: exit('6-20');
21..50: exit('21-50');
else exit('50+');
end;
end;
}
```
## Error Handling and Performance Monitoring
```al
// Error handling and performance telemetry
codeunit 50302 "Error and Performance Telemetry"
{
procedure ProcessWithTelemetry()
var
CustomDimensions: Dictionary of [Text, Text];
StartTime: DateTime;
ProcessingDuration: Duration;
IsSuccess: Boolean;
begin
StartTime := CurrentDateTime;
// Log process start
CustomDimensions.Add('ProcessName', 'DataImport');
CustomDimensions.Add('StartTime', Format(StartTime));
Session.LogMessage('0020', 'Data import process started', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
// Perform the actual work with error handling
IsSuccess := TryExecuteProcess();
ProcessingDuration := CurrentDateTime - StartTime;
// Log completion with performance metrics
Clear(CustomDimensions);
CustomDimensions.Add('ProcessName', 'DataImport');
CustomDimensions.Add('Duration', Format(ProcessingDuration));
CustomDimensions.Add('Success', Format(IsSuccess));
CustomDimensions.Add('PerformanceCategory', GetPerformanceCategory(ProcessingDuration));
if IsSuccess then begin
Session.LogMessage('0021', 'Data import completed successfully', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end else begin
// Add error context without exposing sensitive data
CustomDimensions.Add('ErrorCategory', GetLastErrorCategory());
Session.LogMessage('0022', 'Data import failed', Verbosity::Error,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
end;
[TryFunction]
local procedure TryExecuteProcess()
begin
// Simulate work that might fail
if Random(10) > 7 then
Error('Simulated processing error');
end;
local procedure GetPerformanceCategory(Duration: Duration): Text
begin
// Categorize performance for analysis
if Duration < 1000 then exit('Fast');
if Duration < 5000 then exit('Normal');
if Duration < 15000 then exit('Slow');
exit('VerySlow');
end;
local procedure GetLastErrorCategory(): Text
var
ErrorText: Text;
begin
ErrorText := GetLastErrorText();
// Categorize errors without exposing sensitive details
if StrPos(ErrorText, 'permission') > 0 then exit('Permission');
if StrPos(ErrorText, 'network') > 0 then exit('Network');
if StrPos(ErrorText, 'database') > 0 then exit('Database');
if StrPos(ErrorText, 'timeout') > 0 then exit('Timeout');
exit('General');
end;
}
```
## API Usage Telemetry
```al
// API performance and usage tracking
codeunit 50303 "API Usage Telemetry"
{
procedure LogAPICall(APIEndpoint: Text; ResponseTime: Duration; Success: Boolean)
var
CustomDimensions: Dictionary of [Text, Text];
begin
// Track API performance and usage patterns
CustomDimensions.Add('APIEndpoint', APIEndpoint);
CustomDimensions.Add('ResponseTime', Format(ResponseTime));
CustomDimensions.Add('Success', Format(Success));
CustomDimensions.Add('PerformanceTier', GetAPIPerformanceTier(ResponseTime));
CustomDimensions.Add('TimeOfDay', GetTimeCategory());
Session.LogMessage('0030', 'API call completed', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
procedure LogFeatureUsage(FeatureName: Text; FeatureVersion: Text; UsageContext: Text)
var
CustomDimensions: Dictionary of [Text, Text];
begin
// Track feature adoption and usage patterns
CustomDimensions.Add('FeatureName', FeatureName);
CustomDimensions.Add('FeatureVersion', FeatureVersion);
CustomDimensions.Add('UsageContext', UsageContext);
CustomDimensions.Add('UserCategory', GetAnonymousUserCategory());
CustomDimensions.Add('SessionId', GetSessionHash());
Session.LogMessage('0031', 'Feature used', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
local procedure GetAPIPerformanceTier(ResponseTime: Duration): Text
begin
if ResponseTime < 500 then exit('Excellent');
if ResponseTime < 2000 then exit('Good');
if ResponseTime < 5000 then exit('Acceptable');
exit('Poor');
end;
local procedure GetTimeCategory(): Text
var
CurrentTime: Time;
begin
CurrentTime := Time;
if CurrentTime < 090000T then exit('EarlyMorning');
if CurrentTime < 120000T then exit('Morning');
if CurrentTime < 140000T then exit('Midday');
if CurrentTime < 170000T then exit('Afternoon');
exit('Evening');
end;
local procedure GetAnonymousUserCategory(): Text
var
UserSetup: Record "User Setup";
begin
// Categorize users without identifying them
if UserSetup.Get(UserId) then begin
if UserSetup."Sales Resp. Ctr. Filter" <> '' then exit('SalesUser');
if UserSetup."Purchase Resp. Ctr. Filter" <> '' then exit('PurchaseUser');
end;
exit('GeneralUser');
end;
local procedure GetSessionHash(): Text
begin
// Create session identifier without exposing user identity
exit(CopyStr(CreateGuid(), 1, 8));
end;
}
```
## Anti-Pattern Examples (What NOT to Do)
```al
// ANTI-PATTERNS: Examples of incorrect telemetry implementation
codeunit 50304 "Telemetry Anti-Patterns"
{
// ❌ WRONG: Logging sensitive customer data
procedure BadExample_SensitiveData(CustomerNo: Code[20]; CustomerName: Text)
var
CustomDimensions: Dictionary of [Text, Text];
begin
// ❌ NEVER log personal or sensitive business data
CustomDimensions.Add('CustomerNo', CustomerNo); // Exposes customer identity
CustomDimensions.Add('CustomerName', CustomerName); // Exposes personal data
CustomDimensions.Add('CreditLimit', '50000'); // Exposes business sensitive data
// This violates privacy regulations and security best practices
Session.LogMessage('9001', 'Customer accessed - BAD EXAMPLE', Verbosity::Normal,
DataClassification::CustomerContent, // Wrong classification
TelemetryScope::ExtensionPublisher, CustomDimensions);
end;
// ❌ WRONG: Excessive logging that impacts performance
procedure BadExample_ExcessiveLogging()
var
Customer: Record Customer;
CustomDimensions: Dictionary of [Text, Text];
begin
// ❌ NEVER log inside tight loops without consideration
Customer.FindSet();
repeat
CustomDimensions.Add('CustomerProcessed', Customer."No."); // Also privacy violation
// This will create thousands of telemetry events and impact performance
Session.LogMessage('9002', 'Processing customer', Verbosity::Verbose,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
until Customer.Next() = 0;
end;
// ❌ WRONG: Poor error logging without context
procedure BadExample_PoorErrorLogging()
begin
// ❌ Logging error without useful context for debugging
Session.LogMessage('9003', 'An error occurred', Verbosity::Error,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, '');
// Missing: Error category, operation context, environmental factors
end;
// ❌ WRONG: Using wrong telemetry scope and classification
procedure BadExample_WrongScope()
var
CustomDimensions: Dictionary of [Text, Text];
begin
CustomDimensions.Add('InternalProcessId', CreateGuid());
// ❌ Wrong scope - should be ExtensionPublisher for app-specific events
// ❌ Wrong classification - internal process data shouldn't be CustomerContent
Session.LogMessage('9004', 'Internal process', Verbosity::Normal,
DataClassification::CustomerContent, // Wrong classification
TelemetryScope::All, // Wrong scope
CustomDimensions);
end;
}
```
## Performance-Conscious Telemetry
```al
// Performance-aware telemetry implementation
codeunit 50305 "Performance Aware Telemetry"
{
var
SamplingRate: Integer;
EventCounter: Integer;
procedure InitializeTelemetry()
begin
SamplingRate := 5; // 5% sampling for high-frequency events
EventCounter := 0;
end;
procedure LogHighFrequencyEvent(EventType: Text)
var
CustomDimensions: Dictionary of [Text, Text];
begin
EventCounter += 1;
// Use sampling to reduce telemetry volume
if (EventCounter mod (100 div SamplingRate)) = 0 then begin
CustomDimensions.Add('EventType', EventType);
CustomDimensions.Add('SamplingRate', Format(SamplingRate) + '%');
CustomDimensions.Add('EventsSinceLastLog', Format(100 div SamplingRate));
Session.LogMessage('PERF001', 'Sampled high-frequency event', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
end;
procedure LogBatchOperation(OperationType: Text; RecordCount: Integer; Duration: Duration)
var
CustomDimensions: Dictionary of [Text, Text];
begin
// Aggregate telemetry for batch operations
CustomDimensions.Add('OperationType', OperationType);
CustomDimensions.Add('RecordCountRange', GetRecordCountRange(RecordCount));
CustomDimensions.Add('DurationCategory', GetDurationCategory(Duration));
CustomDimensions.Add('ThroughputCategory', GetThroughputCategory(RecordCount, Duration));
Session.LogMessage('PERF002', 'Batch operation completed', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
local procedure GetRecordCountRange(RecordCount: Integer): Text
begin
case RecordCount of
0..100: exit('1-100');
101..1000: exit('101-1000');
1001..10000: exit('1001-10000');
else exit('10000+');
end;
end;
local procedure GetDurationCategory(Duration: Duration): Text
begin
if Duration < 1000 then exit('Fast');
if Duration < 5000 then exit('Normal');
if Duration < 30000 then exit('Slow');
exit('VerySlow');
end;
local procedure GetThroughputCategory(RecordCount: Integer; Duration: Duration): Text
var
RecordsPerSecond: Decimal;
begin
if Duration = 0 then exit('Instant');
RecordsPerSecond := RecordCount / (Duration / 1000);
if RecordsPerSecond > 1000 then exit('High');
if RecordsPerSecond > 100 then exit('Medium');
exit('Low');
end;
}
```
## Best Practices Implementation
```al
// Comprehensive best practices example
codeunit 50306 "Telemetry Best Practices"
{
procedure ImplementCorrectTelemetry()
var
CustomDimensions: Dictionary of [Text, Text];
StartTime: DateTime;
ProcessSuccess: Boolean;
begin
StartTime := CurrentDateTime;
// ✅ CORRECT: Use meaningful event IDs and structured messages
// ✅ CORRECT: Use appropriate verbosity levels
// ✅ CORRECT: Use SystemMetadata classification for telemetry
// ✅ CORRECT: Use ExtensionPublisher scope for app events
CustomDimensions.Add('ProcessType', 'CustomerDataSync'); // Business context
CustomDimensions.Add('UserCategory', GetUserCategory()); // Anonymized user info
CustomDimensions.Add('CompanySize', GetCompanyCategory()); // Environmental context
CustomDimensions.Add('FeatureVersion', '2.1.0'); // Version tracking
Session.LogMessage('BP001', 'Customer data sync initiated', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
// Execute process with proper error handling
ProcessSuccess := TryExecuteSync();
// Log completion with comprehensive context
Clear(CustomDimensions);
CustomDimensions.Add('ProcessType', 'CustomerDataSync');
CustomDimensions.Add('Duration', GetDurationCategory(CurrentDateTime - StartTime));
CustomDimensions.Add('Success', Format(ProcessSuccess));
if ProcessSuccess then begin
CustomDimensions.Add('Outcome', 'Success');
Session.LogMessage('BP002', 'Customer data sync completed successfully', Verbosity::Normal,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end else begin
CustomDimensions.Add('Outcome', 'Failed');
CustomDimensions.Add('ErrorCategory', GetErrorCategory());
Session.LogMessage('BP003', 'Customer data sync failed', Verbosity::Error,
DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher,
CustomDimensions);
end;
end;
[TryFunction]
local procedure TryExecuteSync()
begin
// Simulate sync operation that might fail
Sleep(Random(5000));
if Random(10) > 8 then
Error('Simulated sync failure');
end;
local procedure GetUserCategory(): Text
begin
// Return user role category without identifying specific user
exit('Administrator'); // Simplified - implement actual role detection
end;
local procedure GetCompanyCategory(): Text
begin
// Return company size category for environmental context
exit('Medium'); // Simplified - implement actual size detection
end;
local procedure GetDurationCategory(Duration: Duration): Text
begin
if Duration < 2000 then exit('Fast');
if Duration < 10000 then exit('Normal');
exit('Slow');
end;
local procedure GetErrorCategory(): Text
var
ErrorText: Text;
begin
ErrorText := LowerCase(GetLastErrorText());
if StrPos(ErrorText, 'permission') > 0 then exit('Permission');
if StrPos(ErrorText, 'network') > 0 then exit('Network');
if StrPos(ErrorText, 'timeout') > 0 then exit('Timeout');
exit('General');
end;
}
```
## Key Implementation Guidelines
### Event ID Strategy
- Use consistent prefixes for different functional areas
- Keep IDs unique and sequential within your extension
- Document ID meanings for long-term maintenance
### Custom Dimensions Best Practices
- Use descriptive, PascalCase key names
- Keep values concise but meaningful
- Limit dimensions per event (5-10 recommended)
- Use categorical values instead of exact numbers
### Privacy Compliance Checklist
- Never log customer names, addresses, or identifiable information
- Use hashing or anonymization for sensitive identifiers
- Categorize numerical data instead of exact values
- Review all telemetry for privacy violations before deployment
### Performance Considerations
- Avoid telemetry in tight loops
- Use sampling for high-frequency events
- Aggregate data for batch operations
- Monitor telemetry impact on application performance