EurekaLog
Menu

The EurekaLog Developer Guide

7. Events: the callbacks EurekaLog calls in your code

Use an event when the automatic reporting path is right but one step needs your application knowledge: add the operation in progress, choose whether to process an exception, attach a diagnostic file, or adjust a sending action. You do not need to replace the exception manager to do any of these.

EEvents provides the callback types and registration routines. Start with OnExceptionNotify for the overall handling decision, OnCustomDataRequest for report fields, or OnExceptionAction for one processing stage. The remaining sections cover the full event surface, including the less obvious password and dialog callbacks. Leaks and freezes use different entry paths, explained in section 7.8.

Before writing a callback, settle two questions: what thread may call it, and what data will still be alive? A form method is not automatically a main-thread callback, and registering a method does not extend the lifetime of its object.

7.1 What an event is here

Each event has a method type ending in ...Meth (of object) and a procedural type ending in ...Proc (a plain procedure with a leading const ACustom: Pointer). Choose a method when you have a suitable live owner, or a plain procedure when the handler must outlive forms. Both can be registered through code; TEurekaLogEvents uses method handlers (chapter 9).

The chain: ACallNextHandler

Every handler, whatever else it takes, ends with var ACallNextHandler: Boolean: True - the default - means call the next registered event handler, False means stop here. Several handlers may be registered for one event, so an event is a chain and this flag is how you cut it. Three events add a companion rule. OnExceptionNotify: set ACallNextHandler to False if you set AHandle to False - having decided that EurekaLog should not process the exception, letting the next handler turn that decision back on is probably not what you meant. OnExceptionAction has the same tip for AExecute, and OnExceptionError (section 7.3) gives the corresponding advice when setting ARetry to True.

The thread you are called on

The first rule applies to every event:

Any event is called from the context of exception thread, which may be not the main thread of application. Thus, it's highly recommended to avoid using any global-like data and to avoid access VCL objects. If you still need to do any of that - please use proper thread synchronization for globals and Synchronize method for VCL access.

Never assume the thread your handler runs on. Event handlers can be called from a background thread even in a single-thread application: a stack overflow makes EurekaLog create a background service thread to handle it. If EurekaLog is enabled for background threads, a handler is called in the context of the thread where the exception happened, so forms are off limits unless you marshal the call with SendMessage, TThread.Synchronize or TThread.Queue. Arguments stashed in a form field and synchronised afterwards can be lost when a second thread overwrites the field before the synchronised method runs. They have to travel with the marshalled call (SendMessage passes them in the message).

A handler that touches nothing global needs none of this; a global that never changes can be read from any thread, but only for as long as it stays valid - a global that is finalised before the handler stops being registered does not qualify - while one that can change while the handler is registered needs real synchronisation.

A handler that changes options should change the local ones (AExceptionInfo.Options) rather than the global ones (CurrentEurekaLogOptions, from EModules) - which also keeps it clear of the trap from chapter 6, where calling the global options without active EurekaLog ends the process.

Handlers registered in a unit versus handlers registered on a form

The component is the other way in: TEurekaLogEvents, from the EComponent unit, dropped on a form and used through the event properties it publishes. Dropping it is "not enough to add EurekaLog to your application" - the project has to be configured for EurekaLog as well - and the unit advises against relying on it: a handler attached through the component "will be active only when corresponding form is live (created)", so it is not called before the form exists or after it is destroyed, which is why registering from code is the recommended route.

7.2 Registering and unregistering a handler

Registration is one call per event - RegisterEvent<Name> - and its mirror UnRegisterEvent<Name>. Every one of the sixteen events has both, and they share one shape; OnRaise is the example (the report-generation pair shares a handler type but not the routines - RegisterEventBeginReportGen and RegisterEventEndReportGen):

procedure RegisterEventRaise(
  const AHandler: TELEvRaiseMeth;
  const AFirst: Boolean = False
); overload;   // EEvents

procedure RegisterEventRaise(
  const ACustom: Pointer;
  const AHandler: TELEvRaiseProc;
  const AFirst: Boolean = False
); overload;   // EEvents

The first overload takes a method handler, the second a procedural one and adds ACustom: "This pointer is not used by EurekaLog, so you can use it for your own goals. Can be nil if not used." The handler must be assigned, and AFirst True inserts it as first where False - the default - inserts it as last, the same ordering ACallNextHandler walks. The tip that comes with every registration routine: "It's best to call this routine in object's constructor (for method event handlers) or unit's initialization section (for procedural event handlers)."

procedure UnRegisterEventRaise(
  const AHandler: TELEvRaiseMeth
); overload;   // EEvents

procedure UnRegisterEventRaise(
  const ACustom: Pointer;
  const AHandler: TELEvRaiseProc
); overload;   // EEvents

Unregistering takes the same handler again; for a procedural handler, ACustom must be the same pointer that was passed to the registration routine. If the handler was not registered, the call does nothing. The mirror of the tip is destructor for method handlers, unit's finalization section for procedural ones. Here is a complete unit that adds a fixed context value to exceptions reaching OnExceptionNotify. Add the unit to a project already configured for EurekaLog. It reads no form or mutable global, preserves the processing and chaining decisions, and unregisters during finalization.

unit ReportContext;

interface

implementation

uses
  EException, EEvents;

procedure AddReportContext(const ACustom: Pointer;
  AExceptionInfo: TEurekaExceptionInfo;
  var AHandle, ACallNextHandler: Boolean);
begin
  if AExceptionInfo <> nil then
    AExceptionInfo.AdditionalInfo := 'Application diagnostic context';
end;

initialization
  RegisterEventExceptionNotify(nil, AddReportContext);
finalization
  UnRegisterEventExceptionNotify(nil, AddReportContext);
end.

AdditionalInfo can replace the dialog message when UseExceptionComments is enabled (section 8.9); for a named field in the report, use OnCustomDataRequest instead. This example is an ordinary exception subscription, not a shutdown-leak subscription.

For a method handler, unregister before its owner is destroyed. The documented FormCreate/ FormDestroy pairing makes that unregistration mandatory. Keep the same ACustom pointer for a procedural subscription's register/unregister pair; EurekaLog does not manage the data behind it. The third routine family, DoEvent<Name>, raises events manually and is normally used by EurekaLog itself rather than application code.

7.3 The events around an exception

OnRaise

TELEvRaiseMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventRaise

Fired for each raised exception. The documented uses are to collect additional information about the exception, to change the information already there, or to place any other custom logic - the sample handler does one line of that: AExceptionInfo.AdditionalInfo := 'Sample additional data';. The parameter is documented as optional and "can be nil", so a handler that touches it should check it first.

Important: "Do not place exception processing code into event handler for this event. Use OnExceptionNotify event instead." The reason is timing: OnRaise fires when EurekaLog detects that a new exception is raised, which can happen before or after the CPU actually throws it, so not all information may be collected yet - the documentation's example is a nil call stack, because building one needs the CPU context record, which does not exist until the throw happened. The event is also wrapped in OnExceptionAction calls with atCollectingExceptionInfo and atCollectedExceptionInfo.

OnExceptionNotify

TELEvExceptionNotifyMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  var AHandle: Boolean;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventExceptionNotify

The handling event. By default it fires for any exception the code did not handle; with the Catch handled exception option enabled it fires for handled ones too, and you tell them apart by AExceptionInfo.Handled. AHandle decides who processes the exception - True (default) EurekaLog, False the standard exception handler. Set ACallNextHandler := False when you set AHandle := False. Exception filters offer another way to make that decision; OnExceptionAction is for individual processing stages. The following is control-flow illustration, not a paste-ready routine: Exception comes from System.SysUtils, the ellipses represent application code, and the first exception must escape to an enabled EurekaLog handling boundary. It assumes "Catch handled exceptions" is off.

try
  raise Exception.Create('Error');   // - OnRaise event
finally
  ...
end;
// - OnExceptionNotify will be called

try
  raise Exception.Create('Error');   // - OnRaise event
except
  ...
end;
// - OnExceptionNotify will not be called, since exception was handled by the code

OnExceptionAction

TELEvExceptionActionMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  const AEurekaAction: TEurekaActionType;
  const AAdditionalInfo: String;
  var AExecute: Boolean;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventExceptionAction

Fired for any action with the exception during its processing - saving to a bug report, sending via the Internet, showing the dialog, and the rest. Use it to alter actions, cancel or skip them. AEurekaAction says which action is in question; AAdditionalInfo carries extra information about it and is non-empty only for atSending and atSent, where it holds the send engine class name - useful if you use several send methods, for example HTTP and e-mail. The type is ETypes.TEurekaActionType, and it reaches beyond a live exception: atEurekaLogInitializing and atEurekaLogInitialized fire with no exception information at all (the first documented as an action in which a handler must not read the global options), atExceptionHandling and atExceptionHandled bracket the processing itself, and atTerminating fires before the application ends. So AExceptionInfo "can be nil" here too.

AExecute means two different things depending on the flavour of the action. For before-like actions, True (default) performs the action and False cancels or skips it - that is how you skip sending, skip the dialog, skip saving. For after-like actions it is the handler that states the outcome: True makes EurekaLog consider the action successful, False unsuccessful, which is how a failure is set aside to "skip errors". For atSendCancelled it is ignored. The documentation describes the loop around it: a before call decides whether the action runs, the action then runs or is recorded as cancelled, an after call sets the outcome, and a failure goes to OnExceptionError for the retry decision.

OnExceptionError

TELEvExceptionErrorMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  const AEurekaAction: TEurekaActionType;
  const AAdditionalInfo: String;
  var ARetry: Boolean;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventExceptionError

The companion of the previous event: fired when an operation failed during exception processing. AEurekaAction is the failed action, AAdditionalInfo is "the reason why action has failed" - the secondary error that failed the step, while the exception being processed is the original one and may be nil - and ARetry decides: True repeats the failed action, False (default) fails the operation. Two action values arrive only here - atEventHandler, when a handler of yours raised the exception, and atGeneral, when exception processing itself failed - and for both the retry argument is documented as ignored. Set ACallNextHandler to False if you set ARetry to True.

Two warnings come with ARetry: "Be sure to do something before switching ARetry to True. Repeating failed action without any attempt to correct settings / environment will simply fail again." and "Be careful when setting ARetry to True: the failed action will be repeated and may raise the same error again. Ensure you have some sort of check to avoid infinite loop."

The order they fire in

The following is a typical event order, not a guaranteed sequence:

Raise stage
  OnExceptionAction      atCollectingExceptionInfo
  OnRaise
  OnExceptionAction      atCollectedExceptionInfo
  Debugger notification about exception

Handle stage
  OnExceptionNotify
  OnCustomBugID
  OnCustomFileName       ftSavedBugReport, ftBugReport, ftZIP, ftZIPCopy, ftXML,
                         ftScreenshot, ftHTMLPage, ftMiniDump  (in that order)
  OnCustomDataRequest
  OnExceptionAction      atSavingLogFile
  OnBeginReportGen
  OnEndReportGen
  OnExceptionAction      atSavedLogFile
  OnExceptionAction      atShowingExceptionInfo
  OnReproduceRequest
  OnExceptionAction      atShowedExceptionInfo
  OnExceptionAction      atSavingLogFile
  OnExceptionAction      atSavedLogFile
  OnZippedFilesRequest
  OnAttachedFilesRequest
  OnExceptionAction      atSending
  OnCustomWebFieldsRequest
  OnExceptionAction      atSent
  OnExceptionAction      atRestarting

7.4 Events that put content into the report

OnCustomDataRequest

TELEvCustomDataRequestMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  ALogBuilder: TObject;
  ADataFields: TStrings;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventCustomDataRequest

Invoked before saving the bug report, to let you supply custom fields as name=value pairs, in the construct the documentation recommends: ADataFields.Values['Field-Name'] := 'Field-Value';. The field name is arbitrary and chosen by you. This is the dynamic counterpart of AddCustomDataToBugReport from chapter 6: that call fixes a value, this event computes one each time a report is written.

Warning: "Do not confuse custom fields (OnCustomDataRequest event) and web fields (OnCustomWebFieldsRequest event). Custom fields are inserted into bug report file. Web fields are not stored in bug report, web fields are POST-ed to bug tracker / web-site via API or HTTP request."

OnCustomBugID

TELEvCustomBugIDMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  var ABugID, ACustomBugID: Word;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventCustomBugID

Use it to override BugID creation. ABugID is the "primary BugID generated by EurekaLog" and is in/out, so you may replace it; ACustomBugID is an output for a sub-ID of your own - define the sub-ID, or replace EurekaLog's value outright.

BugID is a grouping key, not proof of identical root cause. Prefer the custom part when you need to separate failures the normal grouping combines; section 8.10 explains the hash inputs and CustomID.

OnReproduceRequest

TELEvReproduceRequestMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  var AReproduceText: String;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventReproduceRequest

Assigns or modifies the "steps to reproduce" text of the report. Append run-time information: the state, the document, or what the user was actually doing. Use OnCustomDataRequest instead when the information belongs in named custom fields.

OnCustomFileName

TELEvCustomFileNameMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  const AFileType: TEurekaLogFileType;
  var AFileName: String;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventCustomFileName

Use it to assign or modify file names. AFileType is ETypes.TEurekaLogFileType and says which name is being decided: the sequence above lists the kinds in the order they arrive, ftSavedBugReport (the report in its final location) and ftBugReport (the working copy used for sending) first, then the ZIP, XML, screenshot, HTML page and mini dump - ftZIPCopy is the "Save copy in case of send failure" name - and the enumeration holds one the sequence never raises: ftLog, the .csl log file. AFileName is the actual name, which you receive and may change, as a full absolute path. The advice: "Even though you can relocate file in arbitrary folder - it's highly recommended to keep original folder and extension."

OnBeginReportGen and OnEndReportGen

TELEvReportGenMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  ALogBuilder: TObject;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventBeginReportGen / RegisterEventEndReportGen

These events bracket report generation. ALogBuilder is a TBaseLogBuilder instance, passed as TObject by the EEvents declaration above. Include ELogBuilder and cast when you need builder members. The similarly named handler type in ELogBuilder declares the parameter as TBaseLogBuilder directly; do not assume the two declarations are interchangeable when assigning a handler to an EEvents registration routine or component event. Use the signature required by that registration point. Neither handler declaration has an AOptions parameter; do not add one.

OnPasswordRequest

TELEvPasswordRequestMeth = procedure(
  const AModule: HMODULE;
  const APID: PGUID;
  const APasswordHash: Pointer;
  var APassword: String;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventPasswordRequest

The event EurekaLog uses when debug information was saved encrypted: it asks your program for the password that decrypts it. Without one, "all the data will be store in the bug report file in encrypted form, also error dialogs will show bug report in encrypted form" - and "It's impossible to decrypt encrypted debug information without a password." The way back afterwards is the EurekaLog Viewer tool, which can decrypt encrypted bug reports. Your handler may ask the user at run time or read the password from the application's configuration; the point is that the password is, "by default", not stored in your executables.

AModule is the module whose password is being retrieved; APID, the unique project ID, and APasswordHash, the SHA1 hash of the UTF8-encoded password (a pointer to EHash.TSHA1Hash), are "always assigned for EurekaLog 7+ modules" and let you pick the password from a cache or validate what you supply. The event will not be called if the password was obtained by some other means, for example from a local cache, or supplied on the command line with --el_password=.

7.5 Events that add files to the report

OnAttachedFilesRequest

TELEvAttachedFilesRequestMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  const ATempFolder: String;
  AAttachedFiles: TStrings;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventAttachedFilesRequest

Lets you send arbitrary files with the bug report - "Files will be send as separate files." AAttachedFiles is the list you fill in, and the file names must be full absolute paths. ATempFolder is the working folder offered to you - a name with a trailing path delimiter - and "All files in this folder will be deleted automatically."

OnZippedFilesRequest

TELEvZippedFilesRequestMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  const ATempFolder: String;
  AAttachedFiles: TStrings;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventZippedFilesRequest

Use this event for files that belong inside the packed report (.elp). Its parameters and full-path/temporary-folder rules are the same as OnAttachedFilesRequest, whose files travel separately. Do not rely on a temporary file after processing: EurekaLog deletes files in ATempFolder automatically. Review attachments for secrets and personal data before including them (chapter 3).

7.6 The event around sending

OnCustomWebFieldsRequest

TELEvCustomWebFieldsRequestMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  ASender: TObject;
  AWebFields: TStrings;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventCustomWebFieldsRequest

Invoked before sending, to supply custom fields for the request that leaves the machine - the dynamic counterpart of AddCustomWebField from chapter 6. AWebFields takes name=value pairs in the same AWebFields.Values['Field-Name'] := 'Field-Value' form, but with one difference in kind: the field names are not yours to choose freely, they depend on the send method. Sending to Mantis, for instance, the name is the custom field name as it appears in the Mantis configuration, "e.g. like 'cf_count'". ASender is the current sender instance, whose class descends from TELUniversalSender.

Three notes bound this event. It is not called at all if the send method does not need or support custom fields. AExceptionInfo is missing (nil) when sending is performed outside exception processing - that is, when the sending was invoked manually. And EurekaLog calls the event only when sending a bug report: if you invoke a sender manually, to send feedback for instance, there is no bug report and the event is not called, so that a handler never sees a sending it was not written for. If your handler can deal with both, set the _AlwaysCallCustomWebFieldsRequest global variable (from the ESendWeb unit) to True at application startup. To tell error reports and feedback apart inside such a handler, pass a custom field in sending options.

Web fields are not stored in the report at all - they are POST-ed to the bug tracker or web site, which is the distinction from section 7.4.

7.7 The events of the error dialog

OnCustomButtonClick

TELEvCustomButtonClickMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  ADialog: TObject;
  var ACloseDialog: Boolean;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventCustomButtonClick

Fired when the end user clicks the custom button in the error dialog, and the place to put your custom action code: the documented example is a button labelled "check for updates", whose handler runs the update check. ADialog is the dialog that is showing - an object whose class descends from TBaseDialog, and ACloseDialog decides what happens next - True closes the dialog after the handler returns, False (the default) leaves it open. One documented fact about the button is why a handler may never fire: "Custom button is shown only when you specify it in options. It's shown for errors (i.e. bug reports)."

OnHelpButtonClick

TELEvHelpButtonClickMeth = procedure(
  AExceptionInfo: TEurekaExceptionInfo;
  ADialog: TObject;
  var ACallNextHandler: Boolean
) of object;   // EEvents - registered with RegisterEventHelpButtonClick

Fired when the end user clicks the Help button in the error dialog. Help is displayed automatically for known exceptions that have a help context. These exceptions are not errors and do not generate bug reports; a known exception without a help context does not show the button. There is no ACloseDialog here: the declaration gives the handler no output parameter for closing the dialog, only the chain flag. Use RaiseExpected to raise a known exception.

7.8 Leaks and freeze: no event of their own

None of the sixteen events above is specific to memory leaks or a frozen application. Both conditions exist in EurekaLog, but neither arrives through an event of its own - and the reasons differ.

Freeze is an exception. Freeze detection raises one: RaiseFreezeException (unit EFreeze) "will construct EFrozenApplication exception, build call stack for the thread identified by AThreadID (and AThreadHandle), and then invokes standard EurekaLog processing (i.e. create bug report, show error dialog, send report, etc.)". It is "used by freeze detection thread to raise exception when freeze is detected" and may be called from any thread. The arguments are optional: AThreadID defaults to 0, meaning "use MainThreadID", and AThreadHandle is that thread's handle; the message defaults to "The application seems to be frozen." and AAntiFreezeMsg replaces it. Freeze detection starts at application startup through InitCheckFreeze, whose implementation FreezeThreadClass picks (chapter 14). A freeze reaches standard processing through an exception rather than an event of its own; no exact sequence of exception events is guaranteed.

Leaks are reported during late shutdown, when ordinary event registrations and much of the RTL have already been finalized. Do not interpret that as proof that every possible deallocation has already happened: some runtime cleanup can occur later (chapters 22 and 26). For a leak callback, the practical rule is stricter: assume ordinary globals are unavailable unless you deliberately preserved their lifetime. Even SysUtils.Format may fail after FormatSettings is finalized. Do not raise exceptions from this path.

Since general handlers are written without those restrictions, they are not run during leak checking. The documented way to still take part is to re-register handlers at the moment the leaks are shown: your unit's finalization section saves the current handler of the MemLeaksShow variable (from ETypes) and replaces it with a procedure that first registers OnExceptionNotify / OnExceptionAction handlers and then calls the saved original. This is an integration sketch, not a complete unit: YourHandler1 and YourHandler2 must be supplied with the signatures in section 7.3, and the containing unit needs ETypes, EEvents and EException. The sketch assumes the normal initialized MemLeaksShow handler; no general nil guarantee is specified for that variable.

var
  ShowMemLeaks: TMemLeaksProc;   // the default EurekaLog code that shows/processes leaks

procedure AssignEventHandlersForLeaks;
begin
  RegisterEventExceptionNotify(nil, YourHandler1);
  RegisterEventExceptionAction(nil, YourHandler2);
  ShowMemLeaks;   // show the leaks, now that our handlers are registered
end;

finalization
  ShowMemLeaks := MemLeaksShow;                  // remember the default
  MemLeaksShow := AssignEventHandlersForLeaks;   // run our code when leaks are found
end.

Leak checking "is always run from the main thread and all background threads should already be finalized, so no synchronization is (usually) necessary". One handler may serve both exceptions and leaks, told apart by the ACustom pointer you chose or by AExceptionInfo.ExceptionClass - 'EMemoryLeak' or 'EResourceLeak'. If a handler needs data that outlives the application, keep that data in storage you manage yourself and release it afterwards from a handler in MemLeaksShutdown, the counterpart of MemLeaksShow. An ordinary String global is finalised before the leak report is written.

CheckHeap uses a different callback, the MemLeaksError handler variable, not an EEvents registration. TMemoryError (ETypes) describes the detected condition: leNone, leOverflow, leMultiFree, leObjectInUse, leInvalidBlock, leCorruption, leHang, leUseAfterFree, leOutOfMemory or leUninstalled. Here leHang means an unresponsive memory manager, not a frozen application. Chapter 12 covers the memory-checking interface; do not attach a similarly named EEvents handler and expect it to receive these notifications.