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
Synchronizemethod 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.