Skip to main content

System Telemetry

System Telemetry is an extensible framework for running periodic system checks or telemetry actions that are not transactional in nature. It is ideal for background health checks, audits, or validations that should run once per login, once per day, or on a custom schedule—without impacting business-critical processes or user experience.

Key Features

  • Non-Transactional: Designed for system health checks, audits, and background validations—not for transactional or business-critical operations.
  • Flexible Scheduling: Each telemetry entry can define its own frequency (e.g., per login, per day), ensuring checks are performed only as needed and minimizing system load.
  • Runs After UI Load: Telemetry actions are triggered after the user interface has fully loaded, not during standard OnAfterLogin events. This prevents interference with the login process and ensures that any data modifications do not disrupt user access.
  • No Job Queue Required: Telemetry runs automatically according to your defined rules, without needing a dedicated job queue entry. However, you may optionally use the job queue if desired.
  • Extensible: Other extensions can add new telemetry types, rules, and subscribe to processing events to implement custom logic.

This approach ensures system checks are performed reliably and efficiently, without the overhead of job queue management or negative impact on user login.

Administration Tool Events

General Ledger Consistency

This telemetry event checks the consistency of the General Ledger once per day for each company, triggered by the first user login via the web client.

note

It can only be executed by users who have at least Indirect G/L Entry read permission. If a user does not have this permission, it does not execute. Put another way, this means it will only execute when the first user with this permission logs in to the company. It does not execute automatically when web services log in, nor does it run for web services or background services.

How it works:

  • The check runs only once per day per company, regardless of how many users log in.
  • Only triggered by users logging in through the web client.
  • If a consistency issue is detected, telemetry is raised and (optionally) email notifications are sent to specified recipients.
  • If no issues are found, no telemetry is emitted and no emails are sent.

Administrators can specify email recipients for notifications in the app settings.

Extending System Telemetry

The example below shows how the General Ledger Consistency extends the System Telemetry Framework.

codeunit 99932 "SystemTelemetryMgt_ZZ_TSL"
{
Permissions = tabledata "G/L Entry" = r;

[EventSubscriber(ObjectType::Codeunit, Codeunit::SystemTelemetryMgt_CO_TSL, OnBeforeInitialiseSystemTelemetry, '', false, false)]
local procedure SystemTelemetryMgt_CO_TSL_OnBeforeInitialiseSystemTelemetry(TelemetryType: Enum SystemTelemetryType_CO_TSL; var EventId: Code[20]; var AppId: Guid; var AppName: Text[250])
var
ModuleInfo: ModuleInfo;
begin
case TelemetryType of
SystemTelemetryType_CO_TSL::"G/L Consistency Checker":
begin
NavApp.GetCurrentModuleInfo(ModuleInfo);
EventId := 'IMP400';
AppId := ModuleInfo.Id;
AppName := CopyStr(ModuleInfo.Name, 1, 250);
end;
end;
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::SystemTelemetryMgt_CO_TSL, OnInitialiseSystemTelemetry, '', false, false)]
local procedure SystemTelemetryMgt_CO_TSL_OnInitialiseSystemTelemetry(TelemetryType: Enum SystemTelemetryType_CO_TSL; var NextRunTime: DateTime; var Severity: Enum Severity)
begin
case TelemetryType of
SystemTelemetryType_CO_TSL::"G/L Consistency Checker":
begin
if NextRunTime = 0DT then
NextRunTime := CreateDateTime(Today, 0T);
Severity := Severity::Critical;
end;
end;
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::SystemTelemetryMgt_CO_TSL, OnProcessTelemetryEntry, '', false, false)]
local procedure SystemTelemetryMgt_CO_TSL_OnProcessTelemetryEntry(sender: Codeunit SystemTelemetryMgt_CO_TSL; TelemetryType: Enum SystemTelemetryType_CO_TSL; var JsonObject: JsonObject; var MessageText: Text; LastDateTimeRun: DateTime; var NextRunTime: DateTime; var RaiseTelemetry: Boolean; var EmailAddresses: Text)
begin
case TelemetryType of
SystemTelemetryType_CO_TSL::"G/L Consistency Checker":
GLConsistencyCheck(sender, JsonObject, MessageText, LastDateTimeRun, NextRunTime, RaiseTelemetry, EmailAddresses);
end;
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::SystemTelemetryMgt_CO_TSL, OnBeforeExportSettingsForTelemetryEvent, '', false, false)]
local procedure SystemTelemetryMgt_CO_TSL_OnBeforeExportSettingsForTelemetryEvent(TelemetryType: Enum SystemTelemetryType_CO_TSL; var JsonObject: JsonObject; var IsHandled: Boolean)
begin
case TelemetryType of
SystemTelemetryType_CO_TSL::"G/L Consistency Checker":
begin
SetupGLConsistency(JsonObject, false);
IsHandled := true;
end;
end;
end;

[EventSubscriber(ObjectType::Codeunit, Codeunit::SystemTelemetryMgt_CO_TSL, OnAfterImportSettingsForTelemetryEvent, '', false, false)]
local procedure SystemTelemetryMgt_CO_TSL_OnAfterImportSettingsForTelemetryEvent(TelemetryType: Enum SystemTelemetryType_CO_TSL; var JsonObject: JsonObject; var IsHandled: Boolean)
begin
case TelemetryType of
SystemTelemetryType_CO_TSL::"G/L Consistency Checker":
begin
SetupGLConsistency(JsonObject, true);
IsHandled := true;
end;
end;
end;

local procedure GLConsistencyCheck(var SystemTelemetryMgt: Codeunit SystemTelemetryMgt_CO_TSL; var JsonObject: JsonObject; var MessageText: Text; LastDateTime: DateTime; var NextRunTime: DateTime; var RaiseTelemetry: Boolean; var EmailAddresses: Text)
var
GLEntry: Record "G/L Entry";
GLInconsistencyLbl: Label 'The G/L is out of balance by %1. ', Comment = '%1= amount';
LastCheckDoneAtLbl: Label 'Last check performed at %1.', Comment = '%1= date/time';
UseStartDate: Date;
LastEntryNo, StartEntryNo : Integer;
begin
if not GLEntry.ReadPermission then
exit;
MessageText := CopyStr(StrSubstNo(LastCheckDoneAtLbl, LastDateTime), 1, MaxStrLen(MessageText));
GetGLBalanceSettingsFromJson(JsonObject, UseStartDate, StartEntryNo, EmailAddresses);

if CheckIfGLBalanceError(GLEntry, LastEntryNo, StartEntryNo) then begin
SystemTelemetryMgt.AddReplaceJsonKeyValue(JsonObject, LastErrorDateTok, UseStartDate);
SystemTelemetryMgt.AddReplaceJsonKeyValue(JsonObject, LastErrorDiffTok, GLEntry.Amount);
SystemTelemetryMgt.AddReplaceJsonKeyValue(JsonObject, LastErrorStartEntryTok, StartEntryNo);
SystemTelemetryMgt.AddReplaceJsonKeyValue(JsonObject, AmtTok, GLEntry.Amount);
SystemTelemetryMgt.AddReplaceJsonKeyValue(JsonObject, DebitAmountTok, GLEntry."Debit Amount");
SystemTelemetryMgt.AddReplaceJsonKeyValue(JsonObject, CreditAmountTok, GLEntry."Credit Amount");
MessageText := CopyStr(StrSubstNo(GLInconsistencyLbl, GLEntry.Amount) + MessageText, 1, MaxStrLen(MessageText));
RaiseTelemetry := true;
end else begin
SystemTelemetryMgt.RemoveJsonKeyValue(JsonObject, LastErrorDateTok);
SystemTelemetryMgt.RemoveJsonKeyValue(JsonObject, LastErrorDiffTok);
SystemTelemetryMgt.RemoveJsonKeyValue(JsonObject, LastErrorStartEntryTok);
SystemTelemetryMgt.RemoveJsonKeyValue(JsonObject, AmtTok);
SystemTelemetryMgt.RemoveJsonKeyValue(JsonObject, DebitAmountTok);
SystemTelemetryMgt.RemoveJsonKeyValue(JsonObject, CreditAmountTok);
end;
NextRunTime := CreateDateTime(Today + 1, 0T);
SystemTelemetryMgt.AddReplaceJsonKeyValue(JsonObject, LastCheckEntryTok, LastEntryNo);
end;

local procedure SetupGLConsistency(var JsonObject: JsonObject; IsImport: Boolean)
var
MailManagement: Codeunit "Mail Management";
EmailAddresses: Text;
JsonToken: JsonToken;
begin
if not JsonObject.Contains(EmailTok) then
JsonObject.Add(EmailTok, '')
else begin
JsonObject.Get(EmailTok, JsonToken);
EmailAddresses := JsonToken.AsValue().AsText();
if IsImport and (EmailAddresses <> '') then
MailManagement.CheckValidEmailAddresses(EmailAddresses);
end;
end;

local procedure CheckIfGLBalanceError(var GLEntry: Record "G/L Entry"; var LastEntryNo: Integer; StartEntryNo: Integer) HasGLBalanceError: Boolean
begin
GLEntry.ReadIsolation(IsolationLevel::ReadCommitted);
if GLEntry.FindLast() then
LastEntryNo := GLEntry."Entry No.";
GLEntry.SetRange("Entry No.", StartEntryNo, LastEntryNo);
GLEntry.CalcSums("Amount", "Debit Amount", "Credit Amount");
HasGLBalanceError := (GLEntry.Amount <> 0) or (GLEntry."Debit Amount" <> GLEntry."Credit Amount");
end;

local procedure GetGLBalanceSettingsFromJson(var JsonObject: JsonObject; var UseStartDate: Date; var StartEntryNo: Integer; var EmailAddresses: Text)
var
JsonToken: JsonToken;
begin
if JsonObject.Get(LastErrorDateTok, JsonToken) then // the idea is to record the day it first went out of balance
UseStartDate := JsonToken.AsValue().AsDate()
else
UseStartDate := Today;
if JsonObject.Get(LastErrorStartEntryTok, JsonToken) then // always use the entry number when from when it first went into error
StartEntryNo := JsonToken.AsValue().AsInteger() + 1
else
if JsonObject.Get(LastCheckEntryTok, JsonToken) then // use the last entry from the previous time so we dont recheck old entries
StartEntryNo := JsonToken.AsValue().AsInteger() + 1;
if JsonObject.Get(EmailTok, JsonToken) then
EmailAddresses := JsonToken.AsValue().AsText();
end;

var
LastErrorDateTok: Label 'lastErrorDate', Locked = true;
LastErrorStartEntryTok: Label 'lastErrorStartEntryNo', Locked = true;
LastErrorDiffTok: Label 'lastGLBalanceAmt', Locked = true;
LastCheckEntryTok: Label 'lastCheckEntryNo', Locked = true;
EmailTok: Label 'emailAddresses', Locked = true;
AmtTok: Label 'amount', Locked = true;
DebitAmountTok: Label 'debitAmount', Locked = true;
CreditAmountTok: Label 'creditAmount', Locked = true;
}