Core Concepts
Before integrating ExeWatch, it's important to understand how data is organized in the platform.
Application
An Application represents your software product. For each app you want to monitor, you create one Application in ExeWatch.
Examples:
- "MyDesktopApp"
- "CustomerPortal"
- "BackOffice"
Customer
A Customer is your end-user or client company. If you sell software to multiple businesses, each one is a separate Customer.
Examples:
- "ACME Corporation"
- "Smith & Associates"
- "john@example.com"
Device
A Device is a single installation of your app. Each computer, server, or browser where your app runs is a Device.
Examples:
- "john@WORKSTATION-01"
- "admin@SERVER-PROD"
- Browser fingerprint
Data Hierarchy
Each Application has multiple Customers. Each Customer has multiple Devices. Each Device sends Log Events.
Quick Start
Get up and running in 5 minutes with these simple steps.
Step-by-Step Setup
Create an Account
Register for free - no credit card required.
Create an Application
In the dashboard, click "New Application" and give it a name.
Get Your API Key
Click "API Keys" to see your key and download the integration code.
Integrate the SDK
Follow the guide below for your platform (Delphi, .NET, JavaScript, or Python). Check out the official sample projects for ready-to-run demos.
Start Monitoring!
Run your app and watch the logs appear in real-time.
What You'll Need
- An ExeWatch account (free tier available)
- Your application source code
- For Delphi: Delphi 10 Seattle or later
- For .NET: .NET 8.0+ on Windows
- For JavaScript: Any modern browser
- For Python: Python 3.7+ (no dependencies)
- 5 minutes of your time
Delphi SDK Integration
Complete guide to integrating ExeWatch into your Delphi applications.
Step 1 Download SDK for Your App Type
Go to your Application's API Keys page to get your API Key and download the SDK. Choose the files based on your application type:
Console / Service / Web Server
Servers, CLI tools, background services
ExeWatchSDKv1.pas
VCL GUI Application (both files)
Windows desktop apps with VCL
ExeWatchSDKv1.pas
+ ExeWatchSDKv1.VCL.pas
FMX / Android (both files)
Multi-platform GUI apps, Android
ExeWatchSDKv1.pas
+ ExeWatchSDKv1.FMX.pas
Older versions or other languages? Use the DLL SDK — same features, zero compiler requirements. Verified with Delphi 5–13, C++Builder, Microsoft Visual C++ (MSVC), and Python ctypes; expected to work with MinGW, Clang, Rust, Go, C# P/Invoke, VBA, and any Windows-FFI-capable runtime. Full MSVC + C++Builder + Delphi samples at github.com/danieleteti/exewatchsamples.
Step 2 Initialize at Startup
Call InitializeExeWatch once when your application starts.
The SDK works with any type of Delphi application: VCL, FMX, console, web servers, Windows services.
Recommended approach: Initialize early with empty customer ID, then set it after reading from config/license file. This ensures you capture startup logs even before knowing the customer.
uses
ExeWatchSDKv1; // + ExeWatchSDKv1.VCL for VCL GUI apps
begin
// Initialize early with empty customer ID
InitializeExeWatch('ew_win_your_api_key', '');
EW.Info('Application starting...', 'Startup');
// Later, after reading license/config file:
CustomerId := LoadCustomerIdFromLicense(); // your function
EW.SetCustomerId(CustomerId);
EW.Info('License validated for: ' + CustomerId, 'License');
end.
For console applications, batch processors, or CLI tools.
Call EW.WaitForSending before exiting — the process may terminate
before the shipper thread has had time to send everything.
program MyConsoleApp;
{$APPTYPE CONSOLE}
uses
SysUtils,
ExeWatchSDKv1;
begin
try
InitializeExeWatch('ew_win_your_api_key', '');
// Read customer from command line or config
if ParamCount >= 1 then
EW.SetCustomerId(ParamStr(1));
EW.Info('Processing started', 'Main');
// Your application logic here...
EW.Info('Processing completed', 'Main');
except
on E: Exception do
EW.Fatal(E.Message, 'Main');
end;
// Wait for the shipper to drain the queue before the process exits
EW.WaitForSending(15);
FinalizeExeWatch;
end.
For web applications (DMVCFramework, Horse, mORMot, etc.). Initialize once at server startup.
program MyWebServer;
{$APPTYPE CONSOLE}
uses
MVCFramework.Console,
Web.WebReq,
ExeWatchSDKv1,
WebModuleU in 'WebModuleU.pas';
begin
// Initialize ExeWatch - customer ID is your server/instance name
InitializeExeWatch('ew_lin_your_api_key', 'production-server-01');
EW.Info('Web server starting on port 8080', 'Startup');
RunServer(8080);
end.
// In your controller:
procedure TMyController.GetCustomers;
begin
EW.StartTiming('get_customers', 'api');
try
// Your logic...
Render(CustomerService.GetAll);
EW.EndTiming('get_customers');
except
on E: Exception do
begin
EW.EndTiming('get_customers',
TJSONObject.Create(TJSONPair.Create('error', E.Message)), False);
raise;
end;
end;
end;
For Windows services. Initialize in ServiceStart event.
procedure TMyService.ServiceStart(Sender: TService; var Started: Boolean);
begin
// Initialize ExeWatch - customer ID from registry or config
InitializeExeWatch('ew_win_your_api_key', '');
EW.SetCustomerId(ReadCustomerFromRegistry);
EW.Info('Service started', 'Service');
Started := True;
end;
procedure TMyService.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
EW.Info('Service stopping', 'Service');
Stopped := True;
// SDK shutdown is automatic
end;
procedure TMyService.Timer1Timer(Sender: TObject);
begin
EW.StartTiming('background_job');
try
ProcessPendingTasks;
EW.Info('Processed %d tasks', [TaskCount], 'Worker');
EW.EndTiming('background_job');
except
on E: Exception do
begin
EW.EndTiming('background_job',
TJSONObject.Create(TJSONPair.Create('error', E.Message)), False);
EW.Error('Job failed: %s', [E.Message], 'Worker');
end;
end;
end;
EW.SetCustomerId().
For server applications, use the server/instance name instead.
Step 3 Start Logging
Use the global EW object to log messages anywhere in your application.
uses
ExeWatchSDKv1;
procedure TMainForm.ButtonClick(Sender: TObject);
begin
// Log different severity levels
EW.Debug('Button clicked', 'UI');
EW.Info('Processing started', 'Core');
EW.Warning('Low memory detected', 'System');
EW.Error('Database connection failed', 'DB');
EW.Fatal('Critical error - shutting down', 'Core');
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
// Set user identity (optional but recommended)
EW.SetUser('user123', 'john@acme.com', 'John Doe');
EW.Info('Application started', 'Startup');
end;
That's it!
Your Delphi application is now sending logs to ExeWatch. Open the dashboard to see your logs in real-time. The SDK automatically handles offline storage, retries, hardware info collection, and exception capture.
Want more? Check out Breadcrumbs to capture the trail of user actions before an error, Performance Timing to measure operation durations, and Custom Metrics to track counters and gauges.
Runnable samples: DelphiVCL (end-to-end demo), SpecificScenarios/ (focused how-tos: breadcrumbs, madExcept integration, initial device info), DelphiWebBroker & DelphiDMVCFramework (server-side).
.NET / C# SDK Integration
Add monitoring to your .NET applications — WinForms, WPF, console apps, and Windows services. Requires .NET 8.0+ on Windows.
Installation
Add the ExeWatch project reference to your solution. For WinForms apps, also add ExeWatch.WinForms. Download both from the SDK Downloads page.
Initialization
Initialize the SDK at application startup:
using ExeWatch;
// Console / WPF / Service
ExeWatchSdk.Initialize(new ExeWatchConfig
{
ApiKey = "ew_win_your_key",
CustomerId = "ACME-Corp"
});
// WinForms - also install the GUI exception hook
using ExeWatch.WinForms;
ExeWatchWinForms.Install(); // before Application.Run()
// Console / CLI apps: wait for the shipper to drain before exiting
EW.WaitForSending(15);
ExeWatchSdk.Shutdown();
Usage
Use the EW shortcut class anywhere in your code:
using ExeWatch;
// Logging
EW.Info("User logged in", "auth");
EW.Error("Connection failed", "database");
// User identity
EW.SetUser("user123", "john@acme.com", "John Doe");
// Global tags
EW.SetTag("environment", "production");
// Breadcrumbs
EW.AddBreadcrumb("Opened Settings", "navigation");
// Performance timing with try/catch
EW.StartTiming("load_customers", "database");
try
{
var data = LoadCustomers();
EW.EndTiming("load_customers");
}
catch (Exception ex)
{
EW.EndTiming("load_customers",
new Dictionary<string, object> { ["error"] = ex.Message }, false);
throw;
}
// Custom metrics
EW.IncrementCounter("orders_processed");
EW.SetGauge("queue_length", 42);
Features
- Automatic Exception Capture — Unhandled exceptions, WinForms thread exceptions
- Offline Persistence — Logs saved to disk, sent when connection is available
- Hardware Info — CPU, RAM, disks, monitors, OS version
- User Identity — Track users across sessions
- Breadcrumbs — Trail of user actions before errors
- Performance Timing — Measure any operation with StartTiming/EndTiming
- Custom Metrics — Counters and gauges with periodic sampling
- Global Tags — Key-value context for all logs
- Server Config — Remotely adjust flush interval, batch size, log level, sampling
- Zero Dependencies — No NuGet packages required, uses System.Text.Json and System.Net.Http
Supported App Types
| App Type | Package | Exception Capture |
|---|---|---|
| Console | ExeWatch |
AppDomain.UnhandledException |
| Windows Service | ExeWatch |
AppDomain.UnhandledException |
| WinForms | ExeWatch + ExeWatch.WinForms |
Application.ThreadException + AppDomain |
| WPF | ExeWatch |
AppDomain.UnhandledException |
JavaScript SDK Integration
Add monitoring to your web applications in just a few lines of code.
Step 1 Add the Configuration
Add the configuration object to your HTML page, before the SDK script. You'll find your API key in the dashboard under API Keys > JavaScript.
<!-- Add this before </body> -->
<script>
window.ewConfig = {
apiKey: 'ew_web_xxxxxxxxxxxxxxxx',
customerId: 'ACME-Corporation'
};
</script>
Step 2 Include the SDK
Add the SDK script right after the configuration. Use the minified version for production.
<!-- Production (minified, 12KB) -->
<script src="https://exewatch.com/static/js/exewatch.v1.min.js"></script>
<!-- Development (readable, 35KB) -->
<script src="https://exewatch.com/static/js/exewatch.v1.js"></script>
Step 3 Start Logging
Use the global ew object anywhere in your JavaScript code (same as Delphi's EW).
// Set user identity (optional but recommended)
ew.setUser({ id: 'user123', email: 'john@acme.com', name: 'John Doe' });
// Log messages
ew.debug('Page loaded', 'init');
ew.info('User clicked checkout', 'ui');
ew.warning('Slow API response', 'api');
ew.error('Payment failed', 'checkout');
// Add breadcrumbs for debugging
ew.addBreadcrumb('Clicked Buy Now button', 'ui');
ew.addBreadcrumb('Navigated to /checkout', 'router');
// Set global tags
ew.setTag('environment', 'production');
ew.setTag('version', '2.5.0');
Complete Example
<!DOCTYPE html>
<html>
<head>
<title>My Web App</title>
</head>
<body>
<h1>Welcome</h1>
<button onclick="doSomething()">Click me</button>
<!-- ExeWatch Configuration -->
<script>
window.ewConfig = {
apiKey: 'ew_web_xxxxxxxxxxxxxxxx',
customerId: 'ACME-Corporation'
};
</script>
<script src="https://exewatch.com/static/js/exewatch.v1.min.js"></script>
<script>
// Initialize user
ew.setUser({ id: 'user123', email: 'john@acme.com' });
ew.info('Page loaded', 'init');
function doSomething() {
ew.addBreadcrumb('Button clicked', 'ui');
ew.info('User clicked the button', 'ui');
}
</script>
</body>
</html>
Filtering Third-Party Errors (ignoreUrls)
By default, the SDK captures all errors on the page, including failures from third-party scripts
(Google Ads, analytics, fonts, social widgets). Use ignoreUrls to filter out noise from URLs you don't control.
<script>
window.ewConfig = {
apiKey: 'ew_web_xxxxxxxxxxxxxxxx',
customerId: 'ACME-Corporation',
ignoreUrls: [
// Strings: match if URL contains the string
'googlesyndication.com',
'googleadservices.com',
'google-analytics.com',
'googletagmanager.com',
'fonts.googleapis.com',
'facebook.net',
'doubleclick.net',
// RegExp: for more complex patterns
/analytics\..*\.com/,
/cdn\.third-party\.io/
]
};
</script>
<script>, <link>, <img>),
Fetch/XHR network errors, and uncaught JavaScript errors originating from matching URLs.
Breadcrumbs from ignored URLs are also suppressed to keep your trail clean.
Custom HTTP Error Extractor (httpErrorExtractor)
Advanced · Optional
Configure this only when all of the following apply:
- Your backend returns errors in a non-standard JSON schema (not
{ "message": ... },{ "detail": ... }or{ "error": { "message": ... } }) - You want specific fields from that payload (e.g.
exception,statuscode,traceId) promoted to the log title or to dedicated fields inextra_datafor filtering - The default raw
responseBodycapture (already enabled) isn't enough for your workflow
Example — API returns { "error": { "message": "...", "exception": "..." }, "statuscode": 500 }:
window.ewConfig = {
apiKey: 'ew_web_xxxxxxxxxxxxxxxx',
customerId: 'ACME-Corporation',
// Optional: parse your custom error schema
httpErrorExtractor: function(body, contentType, status, url) {
try {
var j = JSON.parse(body);
return {
message: j.error && j.error.message, // shown in log title
extra: { // merged into extra_data
apiException: j.error && j.error.exception,
apiStatusCode: j.statuscode
}
};
} catch (e) { return null; } // fall back to default parsing
}
};
null,
the SDK silently falls back to default parsing and still attaches the raw responseBody.
You never lose error data.
JavaScript SDK Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string | required | Browser API key (starts with ew_web_) |
customerId |
string | required | Customer identifier |
appVersion |
string | null |
App version for release tracking |
autoCapture |
boolean | true |
Auto-capture window errors, fetch/XHR failures, console.error |
ignoreUrls |
Array | [] |
URL patterns (strings or RegExp) to ignore in auto-capture. Matching resource, fetch, XHR, and uncaught errors are silently dropped. |
sampleRate |
number | 1.0 |
Sampling rate (0.0-1.0). Errors are always captured regardless. |
debug |
boolean | false |
Enable SDK debug output in browser console |
enabled |
boolean | true |
Enable/disable the SDK |
batchSize |
number | 10 |
Events per batch before auto-flush |
flushInterval |
number | 5000 |
Auto-flush interval in milliseconds |
httpErrorExtractor Advanced |
Function | null |
Custom parser for non-standard HTTP error response schemas. Signature:
(body, contentType, status, url) → { message?, extra? } | null.
See the Custom HTTP Error Extractor section above — most users don't need this.
|
That's it!
Your web application is now sending logs to ExeWatch. The SDK automatically captures browser info, screen size, and generates a persistent device ID.
Want more? Check out Performance Timing to measure operation durations and Custom Metrics to track counters and gauges.
Runnable sample: ExeWatchSamples/JS.
Python SDK Integration
Add monitoring to your Python applications — Django, Flask, FastAPI, CLI tools, services, and scripts. Single-file SDK with zero external dependencies, compatible with Python 3.7+.
Download & Setup
The Python SDK is a single file (exewatch.py) with no external dependencies.
Download it from your Application's API Keys page, or directly:
Copy the file into your project directory and import it. No pip install needed.
Basic Usage
from exewatch import initialize_exewatch, ew, finalize_exewatch
# Initialize once at startup
initialize_exewatch('YOUR_API_KEY', 'YOUR_CUSTOMER_ID', app_version='1.0.0')
# Log anywhere in your application
ew.info('Application started', 'startup')
ew.warning('Disk space low', 'system')
ew.error('Database connection failed', 'db')
# Extra data via keyword arguments
ew.info('Order placed', 'orders', order_id='12345', total=99.99)
# Short-lived script: wait for the shipper to drain the queue before exiting
ew.wait_for_sending(15)
finalize_exewatch()
Exception Capture
try:
risky_operation()
except Exception as e:
ew.error_with_exception(e, 'module', 'Context message')
# Captures exception class, message, and full stack trace
Timing & Metrics
# Performance timing
ew.start_timing('db_query', 'database')
result = db.execute('SELECT ...')
elapsed = ew.end_timing('db_query') # returns ms, logs automatically
# Counters (cumulative)
ew.increment_counter('orders.placed', 1.0, 'shop')
# Gauges (instantaneous values)
ew.record_gauge('memory_mb', 256.0, 'system')
# Periodic gauge (sampled automatically in background)
def get_memory():
import psutil
return psutil.Process().memory_info().rss / 1024 / 1024
ew.register_periodic_gauge('memory_mb', get_memory, 'system')
User Identity & Breadcrumbs
# User identity (attached to all subsequent logs)
ew.set_user('user-42', 'jane@example.com', 'Jane Doe')
# Breadcrumbs (auto-attached to ERROR/FATAL logs)
from exewatch import BreadcrumbType
ew.add_breadcrumb(BreadcrumbType.NAVIGATION, 'nav', 'Opened settings')
ew.add_breadcrumb(BreadcrumbType.CLICK, 'ui', 'Clicked Save')
ew.add_breadcrumb(BreadcrumbType.QUERY, 'db', 'SELECT * FROM users')
# Next error() or fatal() will include these breadcrumbs
# Global tags (attached to all logs)
ew.set_tag('environment', 'production')
ew.set_tag('feature_flag', 'new_checkout')
Your Python application is now sending logs to ExeWatch. The SDK handles offline storage, background shipping, retries, and device info collection automatically.
Platform support: Windows and Linux. Thread-safe with thread-local breadcrumbs. Zero dependencies — uses only the Python 3.7+ standard library.
Delphi 4–XE7 Integration (DLL SDK)
For Delphi 4 through Delphi XE7. Uses a pre-built DLL with a lightweight Delphi import unit — no source compilation required.
Step 1 Download the DLL SDK
Download the DLL package from your Application's API Keys page. It contains everything you need:
ExeWatchSDKv1Imports.pas
— Delphi import unit (Delphi 4+, no dots in name for old compiler compatibility)
ExeWatchSDKv1DLL.dll
— 32-bit DLL
ExeWatchSDKv1DLL_x64.dll
— 64-bit DLL
Add ExeWatchSDKv1Imports.pas to your project. Copy the appropriate DLL next to your executable.
EW* convenience wrappers (EWInfo, EWError, etc.)
handle string conversion automatically for all Delphi versions.
In Delphi 5–2007 (AnsiString) they convert to WideString internally — no manual casting needed.
Step 2 Initialize at Startup
Call EWInitialize once at application startup.
Works with VCL apps, console programs, and Windows services.
Recommended: Initialize early with an empty customer ID, set it after reading from config or license file.
uses
ExeWatchSDKv1Imports;
begin
// Initialize early — empty customer ID, capture startup logs immediately
EWInitialize('ew_win_your_api_key', '');
EWInfo('Application starting...', 'Startup');
// Later, after reading license / config:
CustomerId := LoadCustomerIdFromLicense; // your function
EWSetCustomerId(CustomerId);
EWInfo('License validated for: ' + CustomerId, 'License');
end.
Full VCL application example with unhandled exception capture (manual hook — the DLL version does not include the ExeWatchSDKv1.VCL unit).
uses
..., ExeWatchSDKv1Imports;
procedure TMainForm.FormCreate(Sender: TObject);
begin
if EWInitialize('ew_win_your_api_key', 'ACME-Corp') <> EW_OK then
begin
ShowMessage('ExeWatch init failed: ' + string(EWGetLastErrorStr));
Application.Terminate;
Exit;
end;
// Manual exception hook — replaces ExeWatchSDKv1.VCL unit
Application.OnException := OnAppException;
EWInfo('Application started', 'Startup');
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
ew_Shutdown; // flush and close the SDK
end;
procedure TMainForm.OnAppException(Sender: TObject; E: Exception);
begin
// Pass stack trace if you use JCL / madExcept / EurekaLog; otherwise empty
EWErrorWithStackTrace(E.Message, 'exception', '', E.ClassName);
Application.ShowException(E);
end;
Console applications, batch processors, or CLI tools.
Call ew_WaitForSending before exiting — the process may terminate
before the shipper thread has had time to send everything.
program MyConsoleApp;
{$APPTYPE CONSOLE}
uses
SysUtils,
ExeWatchSDKv1Imports;
begin
try
EWInitialize('ew_win_your_api_key', '');
if ParamCount >= 1 then
EWSetCustomerId(ParamStr(1));
EWInfo('Processing started', 'Main');
// Your application logic here...
EWInfo('Processing completed', 'Main');
except
on E: Exception do
EWFatal(E.Message, 'Main');
end;
// Wait for the shipper to drain the queue before the process exits
ew_WaitForSending(15);
ew_Shutdown;
end.
Windows services. Initialize in ServiceStart, shut down in ServiceStop.
procedure TMyService.ServiceStart(Sender: TService; var Started: Boolean);
begin
EWInitialize('ew_win_your_api_key', '');
EWSetCustomerId(ReadCustomerFromRegistry);
EWInfo('Service started', 'Service');
Started := True;
end;
procedure TMyService.ServiceStop(Sender: TService; var Stopped: Boolean);
begin
EWInfo('Service stopping', 'Service');
ew_Shutdown;
Stopped := True;
end;
// Background job with timing
procedure TMyService.Timer1Timer(Sender: TObject);
var
ElapsedMs: Double;
begin
EWStartTiming('background_job', 'worker');
try
ProcessPendingTasks;
EWEndTiming('background_job', ElapsedMs);
EWInfo('Job completed', 'Worker');
except
on E: Exception do
begin
EWEndTiming('background_job', ElapsedMs);
EWError('Job failed: ' + E.Message, 'Worker');
end;
end;
end;
{$DEFINE EW_DYNAMIC_LOAD} to your project options
before this unit. Then call EWLoadDLL at startup — if the DLL is missing the app still launches
instead of failing immediately.
Step 3 Start Logging
Use the EW* convenience wrappers anywhere in your code. All accept plain Delphi strings.
// Five severity levels — same as native SDK
EWDebug('Entering payment handler', 'UI');
EWInfo('Processing started', 'Core');
EWWarning('Low memory detected', 'System');
EWError('Database connection failed', 'DB');
EWFatal('Critical failure — aborting', 'Core');
// User identity (optional)
EWSetUser('user-42', 'john@acme.com', 'John Doe');
// Breadcrumbs — attached to next Error/Fatal automatically
EWAddBreadcrumb(EW_BT_NAVIGATION, 'router', 'Opened customer details');
EWAddBreadcrumb(EW_BT_CLICK, 'ui', 'Clicked Save');
EWAddBreadcrumb(EW_BT_QUERY, 'db', 'UPDATE customers SET ...');
EWError('Save failed: connection lost', 'DB'); // breadcrumbs attached here
// Performance timing
var
ElapsedMs: Double;
begin
EWStartTiming('load_customers', 'database');
try
RunQuery;
EWEndTiming('load_customers', ElapsedMs);
except
on E: Exception do
begin
EWEndTiming('load_customers', ElapsedMs);
EWError('Query failed: ' + E.Message, 'DB');
end;
end;
// Metrics
EWIncrementCounter('orders.placed', 1, 'shop');
EWRecordGauge('active_connections', ActiveCount, 'db');
// Global tags (attached to all subsequent logs)
EWSetTag('environment', 'production');
EWSetTag('feature_flag', 'new_checkout');
RegisterPeriodicGauge is not available.
Use a TTimer instead — call EWRecordGauge on each tick. Same result.
That's it!
Your legacy Delphi application is now sending logs to ExeWatch via the DLL SDK. All features are available: logging, breadcrumbs, timing, metrics, user identity, global tags, and device info.
Runnable sample: ExeWatchSamples/DelphiWithDLLSDK — complete VCL demo using every DLL SDK feature (Delphi 4–XE7 compatible).
C++Builder Integration (DLL SDK)
Add ExeWatch monitoring to C++Builder VCL applications using the C/C++ header and the dynamic loading API. The same header works with bcc32, bcc64x, MSVC, MinGW, and Clang — no import library format issues.
Step 1 Download the DLL SDK
Download the DLL package from your Application's API Keys page. Add these files to your C++Builder project:
ExeWatchSDKv1.h
— C/C++ header with all function declarations and types
ExeWatchSDKv1.dynload.c
— dynamic loader implementation (add as a project unit)
ExeWatchSDKv1DLL.dll
— 32-bit DLL (copy next to your executable)
ExeWatchSDKv1DLL_x64.dll
— 64-bit DLL
.lib vs .a vs COFF vs OMF)
and lets the app start gracefully even if the DLL is missing.
Just define EW_DYNAMIC_LOAD before the #include and add
ExeWatchSDKv1.dynload.c to the project.
Step 2 Initialize at Startup
Include the header with EW_DYNAMIC_LOAD, load the DLL, then call ew_Initialize.
All string arguments use wide literals (L"...").
Full VCL application example with dynamic DLL loading and unhandled exception capture.
// In your .cpp file — BEFORE any other ExeWatch include
#define EW_DYNAMIC_LOAD
#include "ExeWatchSDKv1.h"
// Also add ExeWatchSDKv1.dynload.c to the project
static const wchar_t* EXEWATCH_API_KEY = L"ew_win_your_api_key";
void __fastcall TMainForm::FormCreate(TObject *Sender)
{
// Load the DLL (searches exe dir and Windows DLL path)
if (ew_LoadSDK() != EW_OK) {
ShowMessage("Failed to load ExeWatch DLL");
Application->Terminate();
return;
}
// Initialize the SDK
if (ew_Initialize(EXEWATCH_API_KEY, L"ACME-Corp", L"") != EW_OK) {
wchar_t errBuf[512];
ew_GetLastError(errBuf, 512);
ShowMessage(String("ExeWatch init failed: ") + errBuf);
Application->Terminate();
return;
}
// Hook unhandled VCL exceptions
Application->OnException = OnAppException;
ew_Info(L"Application started", L"startup");
}
void __fastcall TMainForm::FormDestroy(TObject *Sender)
{
if (ew_IsSDKLoaded()) {
ew_Shutdown();
ew_UnloadSDK();
}
}
void __fastcall TMainForm::OnAppException(TObject *Sender, Exception *E)
{
String msg = String(E->ClassName()) + ": " + E->Message;
ew_ErrorWithStackTrace(msg.c_str(), L"exception", NULL, E->ClassName().c_str());
Application->ShowException(E);
}
Console or batch applications. Use a standard main entry point.
Call ew_WaitForSending before exiting — the process may terminate
before the shipper thread has had time to send everything.
#define EW_DYNAMIC_LOAD
#include "ExeWatchSDKv1.h"
int main()
{
if (ew_LoadSDK() != EW_OK) return 1;
ew_Initialize(L"ew_win_your_api_key", L"ACME-Corp", L"");
ew_Info(L"Processing started", L"main");
// Your logic...
ew_Info(L"Processing completed", L"main");
// Wait for the shipper to drain the queue before the process exits
ew_WaitForSending(15);
ew_Shutdown();
ew_UnloadSDK();
return 0;
}
Windows services. Initialize in ServiceStart and shut down in ServiceStop.
void __fastcall TMyService::ServiceStart(TService *Sender, bool &Started)
{
if (ew_LoadSDK() != EW_OK) { Started = false; return; }
ew_Initialize(L"ew_win_your_api_key", L"", L"");
ew_SetCustomerId(ReadCustomerFromRegistry());
ew_Info(L"Service started", L"service");
Started = true;
}
void __fastcall TMyService::ServiceStop(TService *Sender, bool &Stopped)
{
ew_Info(L"Service stopping", L"service");
ew_Shutdown();
ew_UnloadSDK();
Stopped = true;
}
// Background job with timing
void __fastcall TMyService::Timer1Timer(TObject *Sender)
{
double elapsed;
ew_StartTiming(L"background_job", L"worker");
try {
ProcessPendingTasks();
ew_EndTiming(L"background_job", &elapsed);
ew_Info(L"Job completed", L"worker");
} catch (Exception &E) {
ew_EndTiming(L"background_job", &elapsed);
ew_Error(E.Message.c_str(), L"worker");
}
}
Step 3 Logging, Breadcrumbs, Timing & Metrics
All ew_* functions accept wchar_t* (wide strings). Use L"..." literals or UnicodeString::c_str().
// Five logging levels
ew_Debug(L"Entering payment handler", L"ui");
ew_Info(L"Processing started", L"core");
ew_Warning(L"Low memory detected", L"system");
ew_Error(L"Database connection failed", L"db");
ew_Fatal(L"Critical failure — aborting", L"core");
// User identity (optional)
ew_SetUser(L"user-42", L"john@acme.com", L"John Doe");
// Breadcrumbs — auto-attached to the next Error/Fatal
ew_AddBreadcrumb(EW_BT_NAVIGATION, L"router", L"Opened customer details", NULL);
ew_AddBreadcrumb(EW_BT_CLICK, L"ui", L"Clicked Save", NULL);
ew_AddBreadcrumb(EW_BT_QUERY, L"db", L"UPDATE customers SET ...", NULL);
ew_Error(L"Save failed: connection lost", L"db"); // breadcrumbs attached here
// Performance timing
double elapsed;
ew_StartTiming(L"load_customers", L"database");
try {
RunQuery();
ew_EndTiming(L"load_customers", &elapsed);
} catch (Exception &E) {
ew_EndTiming(L"load_customers", &elapsed);
ew_Error(E.Message.c_str(), L"db");
}
// Counters (cumulative)
ew_IncrementCounter(L"orders.placed", 1.0, L"shop");
// Gauges (point-in-time)
ew_RecordGauge(L"active_connections", (double)ActiveCount, L"db");
// Global tags
ew_SetTag(L"environment", L"production");
ew_SetTag(L"feature_flag", L"new_checkout");
TTimer and call ew_RecordGauge on each tick — identical result.
That's it!
Your C++Builder application is now sending logs to ExeWatch. All features work: logging, breadcrumbs, timing, metrics, user identity, global tags, and device info.
Runnable sample: ExeWatchSamples/CPPBuilderWithDLLSDK — complete VCL demo covering every feature (bcc32 and bcc64x). Also see MSVCWithDLLSDK for a pure C MSVC console example using the same header.
MSVC / C Integration (DLL SDK)
Add ExeWatch monitoring to any Windows C or C++ application built with
Microsoft Visual C++ (cl.exe), MinGW, Clang, or any toolchain
that follows the Windows stdcall ABI.
The same two files — header + dynamic loader — compile unchanged on all of them.
Step 1 Download the DLL SDK
Download the DLL package from your Application's API Keys page and add these files to your project:
ExeWatchSDKv1.h
— C/C++ header (function declarations, types, constants)
ExeWatchSDKv1.dynload.c
— dynamic loader (compile alongside your source — no linker step needed)
ExeWatchSDKv1DLL_x64.dll
— 64-bit DLL (copy next to your executable)
ExeWatchSDKv1DLL.dll
— 32-bit DLL
Compile the loader alongside your source. No import library (.lib) required — dynload.c
resolves everything at runtime via LoadLibrary + GetProcAddress.
cl /EHsc /W4 /nologo /I<sdk-dir> myapp.c <sdk-dir>\ExeWatchSDKv1.dynload.c
Step 2 Initialize at Startup
Define EW_DYNAMIC_LOAD before the header, call ew_LoadSDK() once,
then ew_Initialize(). All string arguments use wide literals (L"...").
Console or batch applications.
Call ew_WaitForSending before exiting — the process may terminate
before the shipper thread has had time to send everything.
#define EW_DYNAMIC_LOAD
#include "ExeWatchSDKv1.h"
#include <cstdio>
// Compile with: cl /EHsc myapp.c ExeWatchSDKv1.dynload.c
int wmain()
{
// Load DLL at runtime — no import library needed
if (ew_LoadSDK() != EW_OK) {
fwprintf(stderr, L"Failed to load ExeWatch DLL\n");
return 1;
}
if (ew_Initialize(L"ew_win_your_api_key", L"ACME-Corp", L"1.0.0") != EW_OK) {
wchar_t err[512] = {};
ew_GetLastError(err, 512);
fwprintf(stderr, L"Init failed: %ls\n", err);
ew_UnloadSDK();
return 1;
}
ew_Info(L"Processing started", L"main");
/* your application logic here */
ew_Info(L"Processing completed", L"main");
// Wait for the shipper to drain the queue before the process exits
int remaining = ew_WaitForSending(15);
if (remaining > 0)
fwprintf(stderr, L"%d event(s) still queued — will retry on next run\n", remaining);
ew_Shutdown();
ew_UnloadSDK();
return 0;
}
Recommended: Initialize early with an empty customer ID, set it after reading from config or license file.
#define EW_DYNAMIC_LOAD
#include "ExeWatchSDKv1.h"
ew_LoadSDK();
// Initialize early — empty customer ID, capture startup events immediately
ew_Initialize(L"ew_win_your_api_key", L"", L"1.0.0");
ew_Info(L"Application starting...", L"startup");
// Later, after reading license / config:
wchar_t customerId[256];
LoadCustomerIdFromLicense(customerId, 256); /* your function */
ew_SetCustomerId(customerId);
ew_Info(L"License validated", L"license");
Windows services. Initialize in ServiceMain, shut down in the stop handler.
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv)
{
/* register service control handler, set status to START_PENDING ... */
ew_LoadSDK();
ew_Initialize(L"ew_win_your_api_key", L"", L"1.0.0");
ew_SetCustomerId(ReadCustomerFromRegistry());
ew_Info(L"Service started", L"service");
/* set status to RUNNING, enter main service loop */
ServiceLoop();
ew_Info(L"Service stopping", L"service");
ew_Shutdown();
ew_UnloadSDK();
}
// Timed background job
void RunBackgroundJob()
{
double elapsed;
ew_StartTiming(L"background_job", L"worker");
if (ProcessPendingTasks() == 0) {
ew_EndTiming(L"background_job", &elapsed);
ew_Info(L"Job completed", L"worker");
} else {
ew_EndTiming(L"background_job", &elapsed);
ew_Error(L"Job failed", L"worker");
}
}
Step 3 Logging, Breadcrumbs, Timing & Metrics
All ew_* functions accept wchar_t*. Use L"..." literals or
wide string buffers. Works identically across MSVC, MinGW, and Clang.
// Five logging levels
ew_Debug(L"Entering payment handler", L"ui");
ew_Info(L"Processing started", L"core");
ew_Warning(L"Low memory detected", L"system");
ew_Error(L"Database connection failed", L"db");
ew_Fatal(L"Critical failure", L"core");
// User identity (optional)
ew_SetUser(L"user-42", L"alice@acme.com", L"Alice Smith");
// Breadcrumbs — auto-attached to the next Error/Fatal
ew_AddBreadcrumb(EW_BT_NAVIGATION, L"nav", L"Opened main screen", NULL);
ew_AddBreadcrumb(EW_BT_CLICK, L"ui", L"Clicked 'Run report'", NULL);
ew_AddBreadcrumb(EW_BT_QUERY, L"db", L"SELECT * FROM customers", NULL);
ew_Error(L"Save failed: connection lost", L"db"); // breadcrumbs attached here
// Performance timing
double elapsed;
ew_StartTiming(L"generate_report", L"reports");
GenerateReport();
ew_EndTiming(L"generate_report", &elapsed);
wprintf(L"Report done in %.1f ms\n", elapsed);
// Counters (cumulative) and gauges (point-in-time)
ew_IncrementCounter(L"reports.generated", 1.0, L"reports");
ew_RecordGauge(L"queue.depth", (double)QueueSize(), L"jobs");
// Global tags (attached to all subsequent events)
ew_SetTag(L"environment", L"production");
ew_SetTag(L"build", L"msvc-x64");
That's it!
Your MSVC application is now sending logs to ExeWatch. The same code compiles unchanged with MinGW, Clang, and any other Windows C/C++ toolchain — no changes to the SDK files required.
Runnable sample:
ExeWatchSamples/MSVCWithDLLSDK
— complete console demo with every feature, includes run_msvc.cmd to build and run in one step.
Initialization Options
The SDK supports multiple initialization modes to fit different scenarios.
Standard Initialization
The simplest way to initialize - provide API key and Customer ID at startup.
// Delphi
Config := TExeWatchConfig.Create('ew_desk_xxx', 'ACME-Corp');
InitializeExeWatch(Config);
EW.SendDeviceInfo;
Deferred Customer ID
Initialize the SDK without knowing the customer ID upfront. This is useful when:
- License validation happens after app startup
- User authentication determines the customer
- You want to track unlicensed installations
// Initialize without customer ID
Config := TExeWatchConfig.Create('ew_desk_xxx', ''); // Empty customer_id
InitializeExeWatch(Config);
// Logs are tracked by device_id until customer is set
EW.Warning('No valid license found', 'license');
// Later, after license validation...
EW.SetCustomerId('ACME-Corp');
EW.Info('License validated successfully', 'license');
Configuration Options
The TExeWatchConfig record supports many options:
| Option | Default | Description |
|---|---|---|
Release |
(empty) | Version tag for release tracking |
AppVersion |
(auto-detect) | App version (auto-detected from executable) |
SampleRate |
1.0 | Sampling rate (0.0-1.0). 1.0 = 100% of events |
BufferSize |
100 | Max events before auto-flush |
FlushIntervalMs |
5000 | Auto-flush interval in milliseconds |
RetryIntervalMs |
30000 | Wait time between retry attempts |
StoragePath |
%LOCALAPPDATA%\ExeWatch | Path for offline log storage |
// Full configuration example
Config := TExeWatchConfig.Create('ew_desk_xxx', 'ACME-Corp');
Config.Release := '2.5.0';
Config.SampleRate := 0.5; // Send 50% of debug/info events
Config.BufferSize := 50;
Config.FlushIntervalMs := 3000;
InitializeExeWatch(Config);
Advanced Customer ID Scenarios
Real-world examples of handling customer identification in different application types.
Customer ID Change at Runtime
Use case: User switches between different accounts/licenses without restarting the application.
procedure TMainForm.FormCreate(Sender: TObject);
var
Config: TExeWatchConfig;
begin
Config := TExeWatchConfig.Create('ew_win_abc123...', 'CUSTOMER-A');
InitializeExeWatch(Config);
// Device info sent with customer_id = 'CUSTOMER-A'
end;
procedure TMainForm.OnUserSwitched(const NewCustomer: string);
begin
// User switches from CUSTOMER-A to CUSTOMER-B
EW.SetCustomerId(NewCustomer);
// Device info is RE-SENT with the new customer_id
// Backend updates the device to belong to the new customer
EW.Info('User switched to ' + NewCustomer, 'auth');
end;
SetCustomerId with a different customer_id,
the SDK automatically re-sends device info with the new customer. The device in the backend is moved to the new customer.
Multi-Tenant Desktop Application
Use case: Desktop SaaS application where customer_id is determined after user login.
var
FCurrentCustomer: string = '';
procedure TMainForm.FormCreate(Sender: TObject);
var
Config: TExeWatchConfig;
begin
// Initialize SDK WITHOUT customer_id
Config := TExeWatchConfig.Create('ew_win_abc123...', '');
InitializeExeWatch(Config);
// Log without customer_id (user not logged in yet)
EW.Info('Application started - waiting for login', 'startup');
end;
procedure TMainForm.OnLoginSuccess(const Username, Company: string);
begin
// After login, set customer_id
FCurrentCustomer := Company;
EW.SetCustomerId(FCurrentCustomer);
// From now on, all logs have customer_id
EW.Info('User logged in: ' + Username, 'auth');
end;
procedure TMainForm.OnLogout;
begin
EW.Info('User logged out: ' + FCurrentCustomer, 'auth');
// Optionally reset customer_id for next user
// EW.SetCustomerId('');
end;
SetCustomerId are associated with the device ID only
(e.g., "john@PC-WORK"). This helps track pre-login activity and unlicensed installations.
Custom Deployment Information
Use case: Track custom deployment metadata like environment (production/staging), database version, or feature flags.
procedure TMainForm.FormCreate(Sender: TObject);
var
Config: TExeWatchConfig;
Environment, DBVersion: string;
begin
// Read configuration from file/registry
Environment := ReadEnvironmentFromConfig; // 'production', 'staging', etc.
DBVersion := GetDatabaseVersion; // '5.2.1'
Config := TExeWatchConfig.Create('ew_win_abc123...', 'CUSTOMER-ID');
// Add custom device info (array of TPair)
Config.InitialCustomDeviceInfo := [
TPair<string, string>.Create('environment', Environment),
TPair<string, string>.Create('db_version', DBVersion),
TPair<string, string>.Create('deployment_region', 'EU-West')
];
InitializeExeWatch(Config);
EW.Info('Application started in ' + Environment, 'startup');
end;
// Update custom info at runtime
procedure TMainForm.OnFeatureFlagChanged(const Flags: string);
begin
EW.SetCustomDeviceInfo('feature_flags', Flags);
// Changes sent on next log batch
end;
environment: production, db_version: 5.2.1, deployment_region: EU-West.
Release Tracking
Track which version of your application generated each log event.
What is Release?
Release is a version identifier that tags every log event with the version of your app that generated it. This lets you correlate errors and issues with specific versions of your software.
Without Release
"We're seeing 50 errors today. Is this a new bug? Was it introduced in the last update? Did we already fix it?"
With Release
"50 errors - all from version 2.4.0. Version 2.5.0 has zero errors. The fix in 2.5.0 worked!"
Release vs AppVersion
The SDK tracks two version-related values. Understanding the difference helps you use them correctly:
| Release | AppVersion | |
|---|---|---|
| What is it | A label you choose to identify this deployment/build | The version number from the executable's metadata |
| How it's set | You set it manually via Config.Release |
Auto-detected from executable (Project > Options > Version Info) |
| Example | "2.5.0", "2024.01.15", "abc123" |
"2.5.0.1234" (FileVersion from EXE) |
| When to use | Track deployments, hotfixes, feature branches | Track exact build numbers |
Release to your semantic version (e.g., "2.5.0") and let AppVersion be auto-detected for the full build number.
How to Set the Release
Delphi
// Set release at initialization
Config := TExeWatchConfig.Create(
'ew_desk_xxx',
'ACME-Corp'
);
Config.Release := '2.5.0';
InitializeExeWatch(Config);
// All logs now tagged with release=2.5.0
EW.Info('App started', 'startup');
JavaScript
// Set release in config
window.ewConfig = {
apiKey: 'ew_web_xxx',
customerId: 'ACME-Corp',
release: '2.5.0' // Version tag
};
// All logs now tagged with release=2.5.0
ew.info('Page loaded', 'init');
What Value Should I Use?
Choose a format that works for your release process:
| Format | Example | Best for |
|---|---|---|
| Semantic Version | "2.5.0", "1.0.0-beta" |
Most applications - clear major.minor.patch numbering |
| Date-based | "2024.01.15", "24.1" |
Continuous deployment, monthly releases |
| Git commit | "abc123f", "main-abc123" |
Frequent deployments, when you need exact code reference |
| Build number | "build-1234", "CI-5678" |
CI/CD pipelines with incrementing build IDs |
Why Track Releases?
- Identify regressions - See if errors started after a specific release
- Verify fixes - Confirm that a bug fix actually reduced errors
- Rollback decisions - Know which version to rollback to if needed
- Gradual rollout - Monitor errors as you deploy to more users
- Compare versions - See if v2.5 is more stable than v2.4
- Support - Ask "what version are you on?" when helping users
Updates Page
Monitor which versions are deployed across your devices and track adoption.
How it Works
The Updates page shows the current version state of every device in your application. Every time a device sends data to ExeWatch (logs, device info), its version information is automatically updated. The page provides stats, filters, and a version distribution chart to help you understand your deployment landscape at a glance.
To inspect a specific device's version history (past upgrades, downgrades), click on the device name to open its detail modal.
AppVersion vs BinaryVersion
ExeWatch tracks two distinct version identifiers for each device. They serve different purposes and do not necessarily change together:
| AppVersion | BinaryVersion | |
|---|---|---|
| What is it | The product version name — a commercial or human-readable label chosen by the developer | The technical build number auto-detected from the executable's file metadata |
| How it's set | Manually via Config.AppVersion or Config.Release |
Auto-detected from executable (FileVersion / ProductVersion) |
| Format | Free-form: can be numeric ("2.5.0"), a codename ("oxygen"), or a mix ("2.0-oxygen") |
Always numeric: "X.Y.Z.W" (e.g. "10.0.0.0") |
| Changes with each build? | Not necessarily. A product version name may stay the same across multiple builds (e.g. "v2.0-oxygen" can ship as binary 2.0.0.1, 2.0.0.2, etc.) | Yes. Every recompile can produce a new build number |
| Example | "2.0-oxygen", "neon", "3.1.0" |
"2.0.0.1234", "10.0.0.0" |
How "Latest Version" is Determined
The Latest Version stat card shows the highest version currently deployed across all devices. Since version identifiers can vary in format, ExeWatch uses a gradual fallback strategy to determine which version is "the latest":
| Priority | Field used | How it compares | When it applies |
|---|---|---|---|
| 1st | BinaryVersion | Numeric comparison of each segment (e.g. 10.0.0.0 > 9.0.0.0) |
Always preferred when available, since it's auto-detected and always numeric |
| 2nd | AppVersion (numeric parts) | Extracts numbers from the string (e.g. "v3.0-neon" → 3.0) and compares numerically |
When BinaryVersion is missing or identical across versions |
| 3rd | Last activity date | The version used by the most recently active device wins | Last resort: when neither BinaryVersion nor AppVersion contain usable numbers (e.g. pure codenames like "oxygen" vs "neon") |
Examples
BinaryVersion differs
Device A: vOxygen (bin 1.0.0.0)
Device B: vNeon (bin 2.0.0.0)
Latest = vNeon (bin 2 > 1)
No binary, numbers in AppVersion
Device A: v2.0-oxygen (no binary)
Device B: v3.0-neon (no binary)
Latest = v3.0-neon (3.0 > 2.0)
Pure codenames, no numbers
Device A: oxygen (seen yesterday)
Device B: neon (seen today)
Latest = neon (more recent activity)
Adoption Percentage
The Adoption stat shows what percentage of your devices are running the latest version (as determined above). A low adoption percentage means many devices are still on older versions — you may want to encourage your customers to update.
Use the Version Distribution chart and the version filter to drill down into which specific versions are still in use and by which customers.
Usage Analytics
Understand how your software is being used. The Usage page provides session counts, device activity, customer rankings, usage patterns, and error rate trends — all computed automatically from your existing log data. No additional SDK configuration needed.
How it Works
Every time your SDK initializes, it generates a unique session ID and sends an "Application started" log. ExeWatch uses this data — along with device IDs, customer IDs, and timestamps — to compute usage analytics automatically.
Navigate to Usage in the app toolbar and select a time period (1d, 7d, 30d, 90d, or All) to explore your application's usage data.
Period Selector
All widgets on the Usage page respect the selected period. The time bucket granularity adapts automatically:
| Period | Time Range | Bucket Size |
|---|---|---|
| 1d | Last 24 hours | 1 hour |
| 7d | Last 7 days | 1 day |
| 30d | Last 30 days | 1 day |
| 90d | Last 90 days | 1 week |
| All | All available data | 1 month |
Each stat card also shows a delta percentage comparing the current period to the previous period of equal length (e.g., last 7 days vs the 7 days before that).
Widgets
Summary Cards
Four stat cards at the top: Sessions (unique app runs), Devices (unique machines), Customers (unique end-users), and Avg Session (average time between first and last log of each session, excluding single-log sessions).
Sessions Over Time
Line chart showing sessions and devices per time bucket. Empty buckets (days/hours with no activity) are shown as zero, so you can see gaps clearly.
Usage Heatmap
A 7×24 grid showing session activity by day of week and hour of day (UTC). Brighter cells mean more sessions. Hover on any cell to see the exact count.
Top Customers
Table ranking your top 10 customers by session count, with device count and last active timestamp. Useful for identifying your most engaged users and spotting customers that have gone silent.
OS Distribution
Doughnut chart showing the top 10 OS versions across active devices. Full OS strings are displayed in a legend below the chart, with device counts and percentages.
Error Rate Trend
Line chart showing the error rate (percentage of ERROR + FATAL logs out of all logs) per time bucket. Use the version dropdown to filter by one or more app versions. Buckets with no traffic show as gaps (not 0%).
Health Monitoring
Each application gets an automatic health status — Healthy, Degraded, or Critical — visible in the dashboard and sidebar. When the status changes you receive an email notification (and a Discord message, if configured).
What is Monitored
Three signals are evaluated over the last 24 hours. The overall status is the worst of the three.
| Signal | What it measures | Degraded (default) | Critical (default) |
|---|---|---|---|
| Error rate | % of error + fatal events over total | ≥ 1% | ≥ 5% |
| Crashes | Number of fatal events | ≥ 1 | ≥ 3 |
| Slow operations Pro & Business | % of timing measurements above the slow threshold | ≥ 5% | ≥ 20% |
If there are fewer than 100 events in 24h, the error rate shows as Insufficient data instead of a potentially misleading percentage. All thresholds are customizable per application in App Settings → Health Thresholds.
Stable Notifications
ExeWatch won't flood your inbox when a metric hovers near a threshold. Three safeguards keep notifications meaningful:
- Hysteresis — once a signal enters a worse state, it needs to improve by a 30% margin before it can recover. For example, an error rate that crossed 1% (Degraded) must drop below 0.7% to return to Healthy.
- Consecutive confirmation — a new status must hold for 3 consecutive checks before it is accepted. A single spike won't change the status.
- Cooldown — after a notification is sent, the next one is held for at least 60 minutes (configurable in App Settings).
Performance Timing
Measure how long operations take in your application. Identify slow queries, API calls, and bottlenecks.
How It Works
- Call
StartTiming('operation_name')before the operation - Call
EndTiming('operation_name')after it completes - View timing statistics in the ExeWatch dashboard (Timing tab)
- Operations exceeding the slow threshold are highlighted
Delphi Example
// Basic timing with try..except
EW.StartTiming('database_query');
try
RunMyDatabaseQuery;
EW.EndTiming('database_query'); // success
except
on E: Exception do
begin
EW.EndTiming('database_query',
TJSONObject.Create(TJSONPair.Create('error', E.Message)), False);
raise;
end;
end;
// With category for grouping
EW.StartTiming('load_customers', 'database');
try
LoadCustomersFromDB;
EW.EndTiming('load_customers');
except
on E: Exception do
begin
EW.EndTiming('load_customers',
TJSONObject.Create(TJSONPair.Create('error', E.Message)), False);
raise;
end;
end;
JavaScript Example
// Basic timing
ew.startTiming('api_call');
await fetch('/api/data');
ew.endTiming('api_call');
// With category
ew.startTiming('render_chart', 'ui');
renderComplexChart();
ew.endTiming('render_chart');
Metrics (Counters & Gauges)
Track numeric values over time with counters and gauges. Monitor API call rates, memory usage, active connections, and more.
What are Metrics?
ExeWatch supports two types of metrics, inspired by Prometheus:
Counter
A value that only increases. Use counters to track how many times something happens.
Examples: API calls, cache hits, orders processed, errors encountered.
Gauge
A value that can go up or down. Use gauges to track the current state of something.
Examples: memory usage, active connections, queue depth, CPU temperature.
How It Works
- Your application records metrics using
IncrementCounterorRecordGauge - The SDK aggregates values locally (min, max, avg, count)
- Every 60 seconds, aggregated data is flushed to the ExeWatch server
- For periodic gauges, the SDK automatically samples registered callbacks at a configurable interval (default 30s)
- View metrics in the Metrics tab of your application dashboard
Delphi Examples
Counters
// Increment by 1 (default)
EW.IncrementCounter('api_calls');
// Increment by a specific value
EW.IncrementCounter('bytes_processed', FileSize);
// With a tag for grouping
EW.IncrementCounter('api_calls', 1, 'network');
EW.IncrementCounter('cache_hits', 1, 'database');
Gauges (manual)
// Record a point-in-time value
EW.RecordGauge('active_connections', ActiveCount);
EW.RecordGauge('queue_depth', Queue.Count, 'jobs');
Gauges (periodic / automatic)
// Register a callback that the SDK samples every 30s automatically
EW.RegisterPeriodicGauge('memory_mb',
function: Double
begin
Result := GetProcessMemoryMB;
end,
'system');
// Unregister when no longer needed
EW.UnregisterPeriodicGauge('memory_mb');
Configuration
// Set gauge sampling interval (default 30s, min 10s)
Config.GaugeSamplingIntervalSec := 15;
JavaScript Examples
// Counters
ew.incrementCounter('page_views');
ew.incrementCounter('api_calls', 1, 'network');
// Manual gauge
ew.recordGauge('items_in_cart', cart.length);
// Periodic gauge (auto-sampled every 30s)
ew.registerPeriodicGauge('dom_nodes', function() {
return document.querySelectorAll('*').length;
}, 'performance');
// Unregister
ew.unregisterPeriodicGauge('dom_nodes');
// Force flush metrics immediately
ew.flushMetrics();
Configuration
// Set gauge sampling interval in config (default 30, min 10)
window.ewConfig = {
apiKey: 'ew_web_xxx',
customerId: 'customer123',
gaugeSamplingIntervalSec: 15
};
'system', 'network', 'database').
Tags appear as filters in the Metrics dashboard, making it easy to focus on specific areas.
User Identity
Associate logs with specific users to understand who is affected by issues.
What is User Identity?
User Identity represents the person currently using your application. This is different from Customer (which represents the company/organization) and Device (which represents the computer).
Customer
The company or organization that purchased/uses your software. Example: "ACME Corporation"
Device
A specific computer or installation. Example: "john@WORKSTATION-01"
User
The individual person logged in. Example: "John Doe (john@acme.com)"
Example scenario: ACME Corporation (Customer) has 50 employees.
Each employee has their own computer (Device). When John Doe logs into the app on his computer,
you call SetUser so all his actions are tagged with his identity.
How to Use SetUser
Call SetUser after the user logs into your application. All subsequent log events will include this user information.
Delphi
// After user logs in successfully
EW.SetUser(
'user-12345', // ID - unique identifier
'john@acme.com', // Email (optional)
'John Doe' // Display name (optional)
);
// Now all logs include user info
EW.Info('Opened invoice #123', 'invoices');
EW.Warning('Slow query detected', 'db');
EW.Error('Failed to save', 'core');
// All these logs will show: user_id=user-12345
// When user logs out
EW.ClearUser;
EW.Info('User logged out', 'auth');
JavaScript
// After user logs in (pass an object)
ew.setUser({
id: 'user-12345',
email: 'john@acme.com',
name: 'John Doe'
});
// All logs now tagged with user
ew.info('Added item to cart', 'cart');
ew.error('Payment failed', 'checkout');
// On logout
ew.clearUser();
What Values Should I Use?
| Parameter | Required | What to use | Examples |
|---|---|---|---|
ID |
Required | A unique, stable identifier for the user. Use your internal user ID from your database. | "user-12345", "USR_abc123", "42" |
Email |
Optional | User's email address. Useful for contacting affected users. | "john@acme.com" |
Name |
Optional | Display name for easier identification in the dashboard. | "John Doe", "J. Smith" |
Why Use User Identity?
- Find affected users - When an error occurs, see who was affected
- Reproduce issues - Follow a specific user's actions to understand what went wrong
- Contact users - Reach out to users experiencing problems
- Usage patterns - Understand how different users use your app
- Support tickets - Link support requests to specific log trails
- Impact analysis - Know how many unique users are affected by a bug
Hardware Info
The SDK automatically collects device hardware information to help you understand the environment where issues occur.
Collected Information (Delphi SDK)
System
- OS type and version
- Hostname and username
- Timezone and locale
- System language
- System boot time
- Local IP addresses
Hardware
- CPU name and architecture
- Physical and logical cores
- Total and available memory
- Disk drives (size, free space, type)
- Monitors (resolution, color depth)
Application
- Executable path
- Working directory
- Command line
- File version (auto-detected)
- Product name
- Company name
Custom Device Info
Add your own key-value data to device info for application-specific context.
// Set custom info at initialization
Config := TExeWatchConfig.Create('ew_desk_xxx', 'ACME-Corp');
Config.InitialCustomDeviceInfo := [
TPair.Create('license_type', 'enterprise'),
TPair.Create('database', 'PostgreSQL 15')
];
InitializeExeWatch(Config);
// Or set/update custom info at runtime
EW.SetCustomDeviceInfo('active_modules', 'accounting,inventory');
EW.SendCustomDeviceInfo; // Send accumulated custom info
// Or set and send in one call
EW.SendCustomDeviceInfo('last_sync', '2024-01-15T10:30:00');
Offline Mode & Persistence
The SDK is designed to never lose logs, even when the network is unavailable or the app crashes.
How It Works
- Buffer - Logs are collected in memory
- Persist - When buffer is full or interval elapses, logs are written to disk
- Ship - Background thread sends files to server
- Retry - Failed sends are automatically retried
- Cleanup - Successfully sent files are deleted
Benefits
- Logs survive app crashes
- Works offline (syncs when online)
- Non-blocking (async background sending)
- Automatic retries on failure
- Oldest logs sent first (FIFO)
%LOCALAPPDATA%\ExeWatch\pending.
You can customize this with Config.StoragePath.
WaitForSending
Flushes the in-memory buffer and blocks until every pending event has been shipped to the server
(or the timeout elapses). Returns the number of events still pending — 0 means fully drained.
When to use it
The SDK ships events asynchronously in a background thread.
For long-running applications (desktop GUI, Windows services)
this is transparent — the thread keeps running and Shutdown handles the final flush automatically.
For short-lived processes — console tools, batch jobs, test suites, CI scripts —
the process may exit before the shipper thread has had time to send everything.
Calling WaitForSending before Shutdown ensures no events are lost.
Use WaitForSending
- Console applications and CLI tools
- Batch processors and import jobs
- Test suites and CI pipelines
- Short-lived scripts (Python, etc.)
- Any process that exits within seconds of its last log
Not needed
- Desktop GUI applications (long-running)
- Windows services (long-running)
- Web servers and background daemons
- Any process that stays alive for minutes or more
Usage by SDK
program MyConsoleApp;
{$APPTYPE CONSOLE}
uses
SysUtils, ExeWatchSDKv1;
var
Remaining: Integer;
begin
InitializeExeWatch('ew_win_your_api_key', 'ACME-Corp');
EW.Info('Batch job started', 'job');
RunBatchJob;
EW.Info('Batch job completed', 'job');
// Block until all events are shipped (timeout: 15 seconds)
Remaining := EW.WaitForSending(15);
if Remaining > 0 then
Writeln('Warning: ' + IntToStr(Remaining) + ' event(s) still queued — will retry on next run');
// Shutdown after WaitForSending — order matters
FinalizeExeWatch;
end.
uses
SysUtils, ExeWatchSDKv1Imports;
var
Remaining: Integer;
begin
EWInitialize('ew_win_your_api_key', 'ACME-Corp');
EWInfo('Batch job started', 'job');
RunBatchJob;
EWInfo('Batch job completed', 'job');
// Block until all events are shipped (timeout: 15 seconds)
Remaining := ew_WaitForSending(15);
if Remaining > 0 then
Writeln('Warning: ' + IntToStr(Remaining) + ' event(s) still queued');
ew_Shutdown;
end.
// Console / batch application example
#define EW_DYNAMIC_LOAD
#include "ExeWatchSDKv1.h"
int main()
{
ew_LoadSDK();
ew_Initialize(L"ew_win_your_api_key", L"ACME-Corp", L"");
ew_Info(L"Batch job started", L"job");
RunBatchJob();
ew_Info(L"Batch job completed", L"job");
// Block until all events are shipped (timeout: 15 seconds)
int remaining = ew_WaitForSending(15);
if (remaining > 0)
fwprintf(stderr, L"%d event(s) still queued — will retry on next run\n", remaining);
ew_Shutdown();
ew_UnloadSDK();
return 0;
}
using ExeWatch;
ExeWatchSdk.Initialize(new ExeWatchConfig { ApiKey = "ew_win_your_api_key", CustomerId = "ACME-Corp" });
EW.Info("Batch job started", "job");
RunBatchJob();
EW.Info("Batch job completed", "job");
// Block until all events are shipped (timeout: 15 seconds)
int remaining = EW.WaitForSending(15);
if (remaining > 0)
Console.Error.WriteLine($"{remaining} event(s) still queued — will retry on next run");
ExeWatchSdk.Shutdown();
from exewatch import initialize_exewatch, ew, finalize_exewatch
initialize_exewatch('ew_win_your_api_key', 'ACME-Corp')
ew.info('Batch job started', 'job')
run_batch_job()
ew.info('Batch job completed', 'job')
# Block until all events are shipped (timeout: 15 seconds)
remaining = ew.wait_for_sending(15)
if remaining > 0:
print(f"{remaining} event(s) still queued — will retry on next run", file=sys.stderr)
finalize_exewatch()
Return value & timeout guidance
| Return value | Meaning |
|---|---|
0 |
All events shipped successfully before the timeout |
> 0 |
Timeout elapsed — that many events are still on disk and will be retried on next run |
A timeout of 10–30 seconds is appropriate for most batch jobs on a normal connection.
For very large event volumes or slow connections, increase accordingly.
Always call WaitForSending before Shutdown / FinalizeExeWatch.
Sampling
Reduce event volume by sampling a percentage of logs. Useful for high-traffic applications.
Configuration
// Send only 10% of debug/info/warning logs
Config := TExeWatchConfig.Create('ew_desk_xxx', 'ACME-Corp');
Config.SampleRate := 0.1; // 10%
InitializeExeWatch(Config);
// These have a 10% chance of being sent
EW.Debug('Trace message');
EW.Info('User action');
// These are ALWAYS sent (100%)
EW.Error('Something failed');
EW.Fatal('Critical error');
Sample Rate Values
| Value | % Sent | Use Case |
|---|---|---|
1.0 |
100% | Default - send everything |
0.5 |
50% | Moderate reduction |
0.1 |
10% | High-volume apps |
0.01 |
1% | Very high volume |
Stack Traces
ExeWatch automatically captures and displays stack traces for all Error and Fatal level logs. The stack trace shows the exact function call chain that led to the error, making it easy to pinpoint the root cause.
What You Get
In the Log Detail modal, Error and Fatal logs show a Stack Trace section with the full call chain:
MainFormU.TMainForm.btnProcessClick (line 87) MainFormU.TMainForm.ValidateInput (line 123) DataModuleU.TDataModule.SaveRecord (line 456) Vcl.Controls.TControl.Click (line 7459)
Compiler Settings
Stack traces require specific compiler settings. The table below shows what to enable for each platform:
| Setting | Where | Win32 | Win64 |
|---|---|---|---|
Map file = Detailed |
Project Options > Linking | Required | Required |
Stack frames = True |
Project Options > Compiling | Required | Not needed |
Debug information = True |
Project Options > Compiling | Required | Not needed |
The .map file must be deployed alongside the .exe. Without it, stack traces show raw memory addresses instead of function names (still useful, can be resolved later with the map file).
What Gets Captured
Stack traces are automatically attached in these scenarios:
| Scenario | How | Win32 | Win64 |
|---|---|---|---|
EW.Error('...') / EW.Fatal('...') |
Direct call from your code | Full stack | Full stack |
EW.ErrorWithException(E) in except |
Called from a try/except block | Full stack | Full stack |
| Unhandled exception (console app) | Automatic via System.ExceptProc |
Full stack | Full stack |
| VCL/FMX GUI exception (unhandled) | Automatic via Application.OnException |
VCL frames only | Full stack |
try/except and call EW.ErrorWithException(E). This captures the full stack trace. Unhandled GUI exceptions on Win32 only show VCL message loop frames due to SEH stack unwinding limitations.
Example: Exception with Stack Trace
try
// your code that might fail
ProcessOrder(OrderId);
except
on E: Exception do
begin
EW.ErrorWithException(E, 'orders', 'Failed to process order');
// optional: re-raise if you want the default error dialog
raise;
end;
end;
Coexisting with madExcept / EurekaLog
Tools like madExcept and EurekaLog install low-level exception
hooks that intercept exceptions before the VCL's Application.OnException.
When that happens, ExeWatch never sees the exception unless you forward it. Better still,
madExcept has already resolved the stack with unit names and line numbers — passing
that string to ExeWatch gives you a symbolicated, searchable stack in the dashboard
without shipping map files alongside your exe.
Inside the madExcept callback, build an extra_data JSON with a
stack_trace field set to madExcept's BugReport, then pass it
to EW.Log(llError, ...). The SDK uses the provided stack as-is and does not
replace it.
// madExcept bridge — forwards every intercepted exception to ExeWatch
// with madExcept's fully-resolved stack trace. Drop this unit into your
// project; the initialization section installs the callback automatically.
unit ExeWatchMadExceptBridgeU;
interface
implementation
uses System.SysUtils, System.JSON, madExcept, ExeWatchSDKv1;
procedure ExeWatchMadExceptHandler(const ExceptIntf: IMEException;
var Handled: Boolean);
var
ExtraData: TJSONObject;
ExClass, ExMsg: string;
E: TObject;
begin
if (ExceptIntf = nil) or (not ExeWatchIsInitialized) then
Exit;
// Prefer the original Exception's class/message; fall back to madExcept's
// fields for hardware faults that arrived before the Delphi wrapper.
E := ExceptIntf.ExceptObject;
if Assigned(E) and (E is Exception) then
begin
ExClass := E.ClassName;
ExMsg := Exception(E).Message;
end
else
begin
ExClass := ExceptIntf.ExceptClass;
ExMsg := ExceptIntf.ExceptMessage;
end;
// Pre-populate extra_data; the SDK honors stack_trace when already set
// and will NOT overwrite it with its own StackWalk-based capture.
ExtraData := TJSONObject.Create;
ExtraData.AddPair('exception_class', ExClass);
ExtraData.AddPair('exception_message', ExMsg);
ExtraData.AddPair('stack_trace', ExceptIntf.BugReport);
EW.Log(llError, ExMsg, 'exception', ExtraData);
// Handled deliberately left untouched: madExcept's own dialog,
// bug-report save, and email flow continue to work.
end;
initialization
// stDontSync: callback may fire on any thread; EW.* is thread-safe.
RegisterExceptionHandler(ExeWatchMadExceptHandler, stDontSync);
finalization
UnregisterExceptionHandler(ExeWatchMadExceptHandler);
end.
A complete, compile-tested sample project (Delphi 12 + 13, Win32 + Win64, madExcept 5.2.0) is available at exewatchsamples » SpecificScenarios/madExceptIntegration — it includes the bridge unit above, a demo form, and a README with variations (slimmer stack-only payload, filtering non-crash exceptions, etc.).
EurekaLog exposes an equivalent callback (RegisterEventExceptionNotify).
The principle is identical: extract EurekaLog's resolved stack and pass it to
EW.ErrorWithException(E, StackTrace, ...).
Graceful Degradation
If the required settings are missing, the log will include a no_stacktrace_reason code:
| Code | Meaning | Fix |
|---|---|---|
| 1 | RtlCaptureStackBackTrace not available | Requires Windows XP or later |
| 2 | Map file not found | Set Map file = Detailed in Linking options, deploy .map with .exe |
| 3 | 0 frames captured | Enable Stack frames + Debug information in Compiling options (Win32) |
| 4 | Platform not supported | Stack traces are currently Windows-only |
Platform Support
| Platform | Status | Notes |
|---|---|---|
| Windows Win64 | Full support | All scenarios work, only map file needed |
| Windows Win32 | Supported | Requires Stack frames + Debug info. Use try/except for GUI exceptions |
| Linux | Planned | — |
Webhooks
ExeWatch can send alert notifications to external services via webhooks. Two types are available:
| Integration | Plan | Description |
|---|---|---|
| Discord | Pro+ | Sends rich embeds to a Discord channel |
| Generic Webhook | Business | Sends JSON POST to any HTTPS endpoint |
Setup
Go to your app's Alerts page and click the Integrations button. Configure the webhook URL and save. ExeWatch will send notifications when alerts fire (log level, timing, or health status changes).
Generic Webhook Payload
Each webhook delivery is a POST request with a JSON body:
{
"event": "alert.fired",
"alert_type": "log_level",
"alert_name": "Error rate alert",
"app_name": "My Application",
"condition": "errors >= 3 in 10 min",
"message": "5 error events in the last 10 minutes",
"app_id": "937b6080-0088-4bde-942d-91552a640e1a",
"timestamp": "2026-03-21T14:30:00.000Z",
"url": "https://exewatch.com/ui/apps/937b6080-.../logs"
}
Alert Types
alert_type | Trigger |
|---|---|
log_level | Error/Fatal log count exceeds threshold |
timing | Slow operations exceed threshold |
health_critical | App health changed to Critical (red) |
health_degraded | App health changed to Degraded (yellow) |
health_recovery | App health returned to Healthy (green) |
Authentication
Each request includes an X-ExeWatch-Token header with a secret token generated when you configure the webhook. Verify this token on your server to confirm the request is from ExeWatch:
# Python example
@app.post("/webhook")
def handle_webhook(request):
token = request.headers.get("X-ExeWatch-Token")
if token != "whsec_your_secret_here":
return Response(status_code=401)
data = request.json()
print(f"Alert: {data['alert_name']} - {data['message']}")
return Response(status_code=200)
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | ExeWatch-Webhook/1.0 |
X-ExeWatch-Token | Your webhook secret |
X-ExeWatch-Event | alert.fired |
Retry Policy
If your endpoint returns a non-2xx status code or is unreachable, ExeWatch retries the delivery up to 3 times with increasing delays (30 seconds, 2 minutes, 5 minutes). After 3 failed attempts, the delivery is marked as failed. Delivery status is tracked internally for troubleshooting.
Testing
Use the Send Test button in the Integrations modal to send a simulated alert to your endpoint. You can choose the alert type to test. The test payload uses the same format as production alerts, with [TEST] prefixed in the message field for easy identification.
TLS / Self-Signed Certificates
ExeWatch accepts self-signed certificates on webhook endpoints. This is useful for development and internal servers.
Log Levels
ExeWatch supports 5 severity levels to help you categorize and filter your logs.
| Level | Method | When to Use |
|---|---|---|
| Debug | EW.Debug() / ew.debug() |
Detailed information for debugging. Usually disabled in production. |
| Info | EW.Info() / ew.info() |
General information about app execution. User actions, state changes. |
| Warning | EW.Warning() / ew.warning() |
Something unexpected but not critical. Deprecation notices, slow operations. |
| Error | EW.Error() / ew.error() |
Errors that affect functionality but don't crash the app. Failed API calls, validation errors. |
| Fatal | EW.Fatal() / ew.fatal() |
Critical errors that crash or severely impact the app. Unhandled exceptions. |
Best Practices
Do
- Use meaningful tags to categorize logs (e.g., "DB", "API", "UI")
- Set user identity when available for easier debugging
- Log important user actions as Info level
- Include context in error messages
- Use breadcrumbs to track user flow
Don't
- Log sensitive data (passwords, credit cards, PII)
- Use Debug level for everything in production
- Log in tight loops (can generate excessive events)
- Ignore the tag parameter - it helps filtering
- Use Fatal for non-critical errors
Privacy & Terms of Service Guide
When you integrate ExeWatch into your application, you collect operational data from your end users. Depending on your jurisdiction, you may need to disclose this in your Terms of Service or Privacy Policy. Below you'll find exactly what data the SDK collects and a ready-to-use template you can adapt.
What Data Does the SDK Collect?
Delphi Native SDK — Desktop, Server, Mobile
Collected automatically:
- Device ID —
username@hostname(e.g.,jack@MYWORKLAPTOP), where username is the logged-in OS user and hostname is the machine name. If your environment uses personal names as OS usernames (e.g., Active Directory), you can optionally setAnonymizeDeviceId := Trueto replace the username with a non-reversible hash (e.g.,a3f2b1c4@MYWORKLAPTOP). This is not required — it is an option available if your privacy policy demands it - System info — OS version, timezone, locale, language
- Hardware — CPU, RAM, disk space, screen resolution
- Application — binary version, optional app version, executable path
- Session ID — random identifier per app run (not tied to user identity)
- Log events — level, message, tag, thread, timestamps
- Performance timing — operation names and durations (if used)
Collected only if you set it:
- Customer ID — identifier you assign (e.g., company name, license key)
- User identity — only if you call
SetUser()with ID, email, or name - Breadcrumbs — user actions trail, only if you call
AddBreadcrumb() - Global tags — key-value pairs you explicitly set with
SetTag() - Custom log messages — only what your code explicitly logs
JavaScript SDK — Browser
Collected automatically:
- Device ID — random 8-character string stored in
localStorage(e.g.,a3f8b2c1). No OS username or hostname is collected. - Browser info — user agent, screen size, language
- Session ID — random identifier per page load
- Log events — level, message, tag, timestamps
- Performance timing — operation names and durations (if used)
- Automatic breadcrumbs — navigation (pushState/replaceState), user clicks (element info), HTTP requests (fetch/XHR method, URL, status). Collected by default when auto-capture is enabled.
Collected only if you set it:
- Customer ID — identifier you assign
- User identity — only if you call
setUser() - Custom breadcrumbs — additional context you add via
addBreadcrumb() - Global tags — key-value pairs you explicitly set
- Custom log messages — only what your code explicitly logs
Where Is Data Stored?
- Servers: ExeWatch data is stored on servers located in the European Union (EU)
- Encryption: All data is transmitted over HTTPS (TLS 1.2+)
- Retention: Data is retained according to your ExeWatch plan (7, 30, or 90 days) and automatically deleted after that period
- Sub-processors: See our Data Processing Agreement (DPA) for the full list of sub-processors
Privacy Policy / ToS Template
Copy and adapt the text below for your application's Privacy Policy or Terms of Service. Replace the placeholders in [brackets] with your own information.
Data controller
[Your Company Name]
[Your registered address]
Email: [your contact email]
[If applicable:] Data Protection Officer: [DPO name and contact]
Overview
[Your Application Name] uses ExeWatch, a third-party application monitoring service provided by bit Time Professionals S.r.l. (Italy), to monitor application performance, detect errors, and improve software quality.
Legal basis for processing
[Choose the legal basis that applies to your situation:]
Option A — Legitimate interest (Art. 6(1)(f) GDPR): The processing of technical diagnostic data is necessary for our legitimate interest in ensuring the reliability, security, and quality of our software. We have conducted a balancing test and determined that this interest is not overridden by your rights and freedoms, as the data collected is limited to technical information and does not include personal content.
Option B — Contractual necessity (Art. 6(1)(b) GDPR): The processing of diagnostic data is necessary for the performance of the contract between you and [Your Company Name], specifically to provide software support, maintain product quality, and fulfill our obligation to deliver a reliable application.
Option C — Consent (Art. 6(1)(a) GDPR): Diagnostic data is collected only with your explicit consent, which you can withdraw at any time. See the "Opting out" section below.
What data is collected
When you use [Your Application Name], the following technical data is automatically collected and sent to ExeWatch servers:
• Device information: computer name (hostname), operating system username (the currently logged-in user, e.g. "jack"), operating system version, CPU model, RAM amount, available disk space, screen resolution. On desktop, server, and mobile platforms, each device is identified as username@hostname (e.g., "jack@MYWORKLAPTOP").
• Application data: application version, session identifier (random, not tied to your identity), timestamps of events
• Diagnostic data: error messages, performance measurements, and operational log entries generated by the application
[If you use SetUser() or Customer ID, add the following:]
Additionally, the following information may be associated with the collected data:
• Account identifier: your license key or company name, used to associate diagnostic data with your organization
• User identifier: [describe what you pass — e.g., username, email, or internal ID]
What data is NOT collected
The monitoring system does not collect: keystrokes, screen content or screenshots, file contents, browsing history, GPS location, passwords, or any personal documents.
Purpose of data collection
This data is collected solely for the following purposes:
• Detecting and diagnosing software errors and crashes
• Monitoring application performance and reliability
• Understanding software usage patterns to improve the product
• Tracking software version adoption across installations
Data storage and retention
Collected data is transmitted over encrypted connections (HTTPS/TLS 1.2+) to ExeWatch servers located in the European Union. Data is retained for a maximum of [7/30/90] days, after which it is automatically and permanently deleted.
Third-party data processor
ExeWatch (bit Time Professionals S.r.l., Italy) acts as a data processor on behalf of [Your Company Name] (data controller). A Data Processing Agreement (DPA) governs the relationship and ensures GDPR compliance. For details, see: https://exewatch.com/ui/dpa
Your rights
Under the General Data Protection Regulation (GDPR) and other applicable data protection laws, you have the following rights regarding your personal data:
• Right of access (Art. 15) — request a copy of the data we hold about you
• Right to rectification (Art. 16) — request correction of inaccurate data
• Right to erasure (Art. 17) — request deletion of your data
• Right to restriction (Art. 18) — request that we limit how your data is processed
• Right to data portability (Art. 20) — request your data in a machine-readable format
• Right to object (Art. 21) — object to processing based on legitimate interest
To exercise any of these rights, contact: [your contact email]
You also have the right to lodge a complaint with your local data protection supervisory authority if you believe your data is being processed unlawfully.
Opting out
[Choose the option that matches your chosen legal basis:]
Option A (for Legitimate interest or Contractual necessity): You may object to the processing of diagnostic data by contacting us at [your contact email]. We will evaluate your request and cease processing unless we can demonstrate compelling legitimate grounds. The monitoring cannot be disabled individually within the application, but all data is handled in strict accordance with this policy and applicable data protection laws.
Option B (for Consent): You may withdraw your consent and disable diagnostic data collection at any time from [Settings > Privacy] in the application, or by contacting us at [your contact email]. Withdrawal of consent does not affect the lawfulness of processing carried out before the withdrawal.
Tips for Compliance
- Avoid logging personal data: Don't pass usernames, emails, or personal info in log messages unless strictly necessary
- Be transparent: If you use
SetUser(), mention it in your Privacy Policy - Minimize data: Only log what you need for debugging. Use sampling to reduce volume
- Consider opt-out: If required by your jurisdiction, provide users with a way to disable monitoring
- Sign the DPA: If you process EU personal data, review and sign our Data Processing Agreement
- Update regularly: Review your Privacy Policy when you add new SDK features (e.g., User Identity, Breadcrumbs)
Tutorials & Resources
Official SDK Samples
Ready-to-run demo projects for Delphi, JavaScript, and more. Clone, configure your API key, and start exploring.
View on GitHubUpdates & Usage Analytics
Blog post covering the Updates page, Usage Analytics, and how to track your application adoption.
Read Blog PostReady to Get Started?
Create your free account and start monitoring in minutes.
Create Free AccountFor bug reports, feedback, or questions: exewatch@bittime.it