Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
sentry.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19#ifdef HAVE_CONFIG_H
20#include "config.h"
21#include "gui/screen_metrics.h"
24#endif
25
26#include "common/sentry.h"
27#include "common/paths.h" // DT_PATH_MAX
28#include "common/times.h"
29
30#include <glib/gstdio.h> // for g_unlink
31
32#ifdef HAVE_SENTRY
33
35#include "common/image.h"
36#include "common/opencl.h"
37#include "common/conf.h"
38
39#include <sentry.h>
40
41#include <signal.h> // for sig_atomic_t
42#include <string.h> // for strrchr, strcmp
43
44#if defined(__linux__)
45#include <sys/prctl.h> // for PR_SET_PTRACER
46#include <sys/wait.h> // for waitpid
47#include <unistd.h> // for fork, getpid
48#endif
49
50#if defined(_WIN32)
51#ifndef WIN32_LEAN_AND_MEAN
52#define WIN32_LEAN_AND_MEAN // limit windows.h macro pollution next to GLib/GTK
53#endif
54#include <windows.h>
55#include <dbghelp.h> // for Sym*, locally symbolicated backtrace
56#endif
57
58#ifndef SENTRY_DSN
59#define SENTRY_DSN ""
60#endif
61
62// Conf keys. "sentry/enabled" is a confgen key (shown in Preferences); the two
63// below are intentionally NOT in confgen so dt_conf_key_exists() reflects whether
64// the user has actually decided / how many clean sessions were recorded.
65#define DT_SENTRY_ENABLED_KEY "sentry/enabled"
66#define DT_SENTRY_ASKED_KEY "sentry/consent_asked"
67#define DT_SENTRY_CLEAN_SESSIONS_KEY "sentry/clean_sessions"
68#define DT_SENTRY_LAST_SESSION_KEY "sentry/last_session_seconds"
69#define DT_SENTRY_TOTAL_SESSION_KEY "sentry/total_session_seconds"
70
71static gboolean _sentry_inited = FALSE;
72
73/* Observers handed the crash backtrace, from inside the crash handler.
74 *
75 * A fixed array: registration happens at startup, and a crash handler must not walk a
76 * structure another thread could be reallocating. Nothing here knows or cares what an
77 * observer is for -- see dt_sentry_add_crash_observer(). */
78#define DT_SENTRY_MAX_CRASH_OBSERVERS 4
79static dt_sentry_crash_observer_t _crash_observers[DT_SENTRY_MAX_CRASH_OBSERVERS] = { NULL };
80static int _crash_observer_count = 0;
81
83{
84 if(IS_NULL_PTR(observer) || _crash_observer_count >= DT_SENTRY_MAX_CRASH_OBSERVERS) return;
85 _crash_observers[_crash_observer_count++] = observer;
86}
87
88/* Called from the crash handler: everything downstream must be async-signal-safe. */
89static void _sentry_notify_crash_observers(const char *backtrace, gsize backtrace_len)
90{
91 for(int i = 0; i < _crash_observer_count; i++)
92 if(_crash_observers[i]) _crash_observers[i](backtrace, backtrace_len);
93}
94
95// Set once sentry's on_crash hook has captured a gdb backtrace, so the local
96// signal handler can skip running gdb a second time for the same crash.
97static volatile sig_atomic_t _sentry_backtrace_captured = 0;
98
99// Per-session module usage counts ("category/name" -> count). Mutated only from
100// the GUI thread; mirrored into the sentry scope on each change so the crash
101// handler never has to read this table (which would be unsafe in a signal context).
102static GHashTable *_module_usage = NULL;
103
104// Dedup state for the currently-processed image. Pipelines run on worker threads
105// (darkroom full/preview, export), so this is guarded by a mutex.
106static GMutex _processed_image_lock;
107static int32_t _processed_imgid = -1;
108static char _processed_pipeline[32] = { 0 };
109// Count of distinct image+pipeline runs this session, stamped on crash events so
110// the website can report "images processed before a crash".
111static volatile int _processed_image_count = 0;
112
113// Length of the running session, in seconds. The start wtime is stamped at
114// the very start of dt_init().
115static double _sentry_session_seconds(void)
116{
117 const double dur = dt_get_wtime() - dt_get_start_wtime();
118 return (dur > 0.0) ? dur : 0.0;
119}
120
121// Stamp the event with the current session length, in seconds. For crashes this
122// runs inside the crashing process, so the value is the exact time-to-crash.
123static void _sentry_stamp_session_length(sentry_value_t event)
124{
125 const double dur = _sentry_session_seconds();
126
127 // Numeric values under "extra" for inspection.
128 sentry_value_t extra = sentry_value_get_by_key(event, "extra");
129 if(sentry_value_is_null(extra))
130 {
131 extra = sentry_value_new_object();
132 sentry_value_set_by_key(event, "extra", extra);
133 }
134 sentry_value_set_by_key(extra, "session_seconds", sentry_value_new_double(dur));
135 sentry_value_set_by_key(extra, "images_processed", sentry_value_new_int32(_processed_image_count));
136
137 // String tags so events are searchable/groupable by session length and by how
138 // many images had been processed when the crash happened.
139 char buf[32];
140 snprintf(buf, sizeof(buf), "%.0f", dur);
141 char ibuf[32];
142 snprintf(ibuf, sizeof(ibuf), "%d", _processed_image_count);
143 sentry_value_t tags = sentry_value_get_by_key(event, "tags");
144 if(sentry_value_is_null(tags))
145 {
146 tags = sentry_value_new_object();
147 sentry_value_set_by_key(event, "tags", tags);
148 }
149 sentry_value_set_by_key(tags, "session_seconds", sentry_value_new_string(buf));
150 sentry_value_set_by_key(tags, "images_processed", sentry_value_new_string(ibuf));
151}
152
153// before_send handles NON-crash events (on_crash takes over for crashes). Stamp
154// the session length so every event carries it.
155static sentry_value_t _sentry_before_send(sentry_value_t event, void *hint, void *user_data)
156{
157 _sentry_stamp_session_length(event);
158 return event;
159}
160
161#if defined(__linux__)
162// Run gdb against the crashing process and capture its backtrace into a freshly
163// allocated buffer (NUL-terminated; *len excludes the terminator). Returns NULL
164// on failure. This mirrors the local gdb fallback in system_signal_handling.c but
165// returns the text so it can be attached to the Sentry crash report.
166static char *_sentry_capture_gdb_backtrace(gsize *len)
167{
168 gchar *name = NULL;
169 const int fd = g_file_open_tmp("ansel_sentry_bt_XXXXXX.txt", &name, NULL);
170 if(fd == -1) return NULL;
171 close(fd);
172
173 gchar *pid_arg = g_strdup_printf("%d", (int)getpid());
174 gchar *exe_arg = g_strdup_printf("/proc/%s/exe", pid_arg);
175 gchar *log_file_arg = g_strdup_printf("set logging file %s", name);
176
177 char *contents = NULL;
178 const pid_t pid = fork();
179 if(pid == 0)
180 {
181 // child: gdb attaches to the parent and dumps all threads' backtraces
182 execlp("gdb", "gdb", exe_arg, pid_arg, "-batch",
183 "-ex", "set pagination off",
184 "-ex", "set confirm off",
185 "-ex", log_file_arg,
186 "-ex", "set logging overwrite on",
187 "-ex", "set logging redirect on",
188 "-ex", "set logging enabled on",
189 "-ex", "thread apply all bt full",
190 NULL);
191 _exit(127); // execlp only returns on failure
192 }
193 else if(pid > 0)
194 {
195 prctl(PR_SET_PTRACER, pid, 0, 0, 0); // let the child ptrace us (Yama)
196 waitpid(pid, NULL, 0);
197
198 gsize n = 0;
199 if(g_file_get_contents(name, &contents, &n, NULL))
200 {
201 if(len) *len = n;
202 }
203 else
204 {
205 contents = NULL;
206 }
207 }
208
209 g_unlink(name);
210 g_free(name);
211 g_free(pid_arg);
212 g_free(exe_arg);
213 g_free(log_file_arg);
214 return contents;
215}
216#endif // __linux__
217
218#if defined(_WIN32)
219// Resolve the short module name owning a given address (e.g. "libansel.dll").
220static void _sentry_module_name(DWORD64 addr, char *out, size_t out_len)
221{
222 g_strlcpy(out, "?", out_len);
223 HMODULE mod = NULL;
224 if(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS
225 | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
226 (LPCSTR)(uintptr_t)addr, &mod)
227 && mod)
228 {
229 char path[MAX_PATH];
230 if(GetModuleFileNameA(mod, path, sizeof(path)))
231 {
232 const char *base = strrchr(path, '\\');
233 g_strlcpy(out, base ? base + 1 : path, out_len);
234 }
235 }
236}
237
238// Capture the crashing thread's backtrace and symbolicate it locally with
239// DbgHelp. Mirrors the Linux gdb capture: returns a NUL-terminated buffer
240// (*len excludes the terminator) to attach to the crash report, or NULL on
241// failure. Local symbolication is the whole point on Windows: self-builds carry
242// matching .pdb files on the user's disk but never upload them to Sentry, so
243// without this their crashes arrive as bare addresses (see #129664611).
244static char *_sentry_capture_windows_backtrace(const sentry_ucontext_t *uctx, gsize *len)
245{
246 if(IS_NULL_PTR(uctx)) return NULL;
247
248 void *frames[128];
249 const size_t n = sentry_unwind_stack_from_ucontext(uctx, frames, 128);
250 if(n == 0) return NULL;
251
252 const HANDLE proc = GetCurrentProcess();
253 SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES | SYMOPT_UNDNAME);
254 // May fail with ERROR_INVALID_PARAMETER if sentry already initialized the
255 // symbol handler for this process; symbols stay usable either way.
256 SymInitialize(proc, NULL, TRUE);
257
258 GString *out = g_string_new(NULL);
259 g_string_append_printf(out, "this is %s reporting a crash (local DbgHelp backtrace, crashing thread):\n\n",
261
262 // SYMBOL_INFO with room for the (undecorated) symbol name right after it.
263 char symbuf[sizeof(SYMBOL_INFO) + 512];
264 SYMBOL_INFO *sym = (SYMBOL_INFO *)symbuf;
265 memset(symbuf, 0, sizeof(symbuf));
266 sym->SizeOfStruct = sizeof(SYMBOL_INFO);
267 sym->MaxNameLen = 512;
268
269 for(size_t i = 0; i < n; i++)
270 {
271 const DWORD64 addr = (DWORD64)(uintptr_t)frames[i];
272
273 char modname[MAX_PATH];
274 _sentry_module_name(addr, modname, sizeof(modname));
275
276 const unsigned long long fno = (unsigned long long)i;
277 DWORD64 disp = 0;
278 if(SymFromAddr(proc, addr, &disp, sym))
279 {
280 DWORD line_disp = 0;
281 IMAGEHLP_LINE64 line;
282 memset(&line, 0, sizeof(line));
283 line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
284 if(SymGetLineFromAddr64(proc, addr, &line_disp, &line))
285 g_string_append_printf(out, "#%-2llu 0x%016llx %s+0x%llx (%s:%lu) [%s]\n", fno,
286 (unsigned long long)addr, sym->Name, (unsigned long long)disp,
287 line.FileName, (unsigned long)line.LineNumber, modname);
288 else
289 g_string_append_printf(out, "#%-2llu 0x%016llx %s+0x%llx [%s]\n", fno, (unsigned long long)addr,
290 sym->Name, (unsigned long long)disp, modname);
291 }
292 else
293 {
294 g_string_append_printf(out, "#%-2llu 0x%016llx ? [%s]\n", fno, (unsigned long long)addr, modname);
295 }
296 }
297
298 if(len) *len = out->len;
299 return g_string_free(out, FALSE);
300}
301#endif // _WIN32
302
303// on_crash replaces before_send for crash events (inproc). It runs before the
304// crash event/attachments are serialized, so this is where we both stamp the
305// session length and attach a full gdb backtrace to the report.
306static sentry_value_t _sentry_on_crash(const sentry_ucontext_t *uctx, sentry_value_t event, void *user_data)
307{
308 _sentry_stamp_session_length(event);
309
310 /* Declared outside the platform branches so the notification below has exactly one call
311 * site. It used to sit inside each branch, which left it unreferenced on any platform that
312 * captures no backtrace -- macOS is one -- and -Werror rightly rejected that. */
313 gsize bt_len = 0;
314 char *bt = NULL;
315
316#if defined(__linux__)
317 bt = _sentry_capture_gdb_backtrace(&bt_len);
318 if(bt && bt_len > 0)
319 {
320 // Registered on the scope, this is picked up when the crash envelope is
321 // assembled (right after this hook returns). sentry copies the bytes.
322 sentry_attach_bytes(bt, bt_len, "gdb-backtrace.txt");
323
324 // Tell the local signal handler (which runs next in the chain) not to run
325 // gdb again for this same crash.
326 _sentry_backtrace_captured = 1;
327 }
328#elif defined(_WIN32)
329 bt = _sentry_capture_windows_backtrace(uctx, &bt_len);
330 if(bt && bt_len > 0)
331 {
332 sentry_attach_bytes(bt, bt_len, "windows-backtrace.txt");
333 // Tell the local exception filter (which runs next in the chain) not to
334 // pop its own backtrace dialog for this same crash.
335 _sentry_backtrace_captured = 1;
336 }
337#endif
338
339 /* Observers get whatever was captured. On a platform with no capture path -- macOS today --
340 * bt stays NULL and nothing is notified, which is correct rather than merely tolerable: an
341 * observer's whole job is to read the backtrace, and there is none to read. Anything keyed
342 * on this (see dt_opencl_note_crash_backtrace) is therefore Linux and Windows only, which
343 * is where the crashes it answers were reported. */
344 if(bt && bt_len > 0) _sentry_notify_crash_observers(bt, bt_len);
345 g_free(bt);
346
347 return event;
348}
349
350gboolean dt_sentry_backtrace_captured(void)
351{
352 return _sentry_backtrace_captured != 0;
353}
354
355void dt_sentry_record_module_usage(const char *category, const char *name)
356{
357 if(!_sentry_inited || !category || !name || !*name) return;
358
359 if(!_module_usage)
360 _module_usage = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
361
362 // g_hash_table_insert frees the duplicate key when the entry already exists,
363 // so the table keeps a single owned copy of each key.
364 char *key = g_strdup_printf("%s/%s", category, name);
365 const int count = GPOINTER_TO_INT(g_hash_table_lookup(_module_usage, key)) + 1;
366 g_hash_table_insert(_module_usage, key, GINT_TO_POINTER(count));
367
368 // Push the whole map into the scope as the "module_usage" context. Cheap at
369 // human interaction rates, and keeps the crash path free of table iteration.
370 sentry_value_t obj = sentry_value_new_object();
371 GHashTableIter iter;
372 gpointer k, v;
373 g_hash_table_iter_init(&iter, _module_usage);
374 while(g_hash_table_iter_next(&iter, &k, &v))
375 sentry_value_set_by_key(obj, (const char *)k, sentry_value_new_int32(GPOINTER_TO_INT(v)));
376 sentry_set_context("module_usage", obj);
377}
378
379void dt_sentry_set_processed_image(const struct dt_image_t *img, const char *pipeline)
380{
381 if(!_sentry_inited || !img) return;
382 const char *pl = pipeline ? pipeline : "";
383
384 // Skip the (frequent) case where the same image keeps being reprocessed by the
385 // same pipeline; only push a new context when something actually changed.
386 g_mutex_lock(&_processed_image_lock);
387 if(img->id == _processed_imgid && !strcmp(pl, _processed_pipeline))
388 {
389 g_mutex_unlock(&_processed_image_lock);
390 return;
391 }
392 _processed_imgid = img->id;
393 g_strlcpy(_processed_pipeline, pl, sizeof(_processed_pipeline));
394 _processed_image_count++;
395 g_mutex_unlock(&_processed_image_lock);
396
397 // Extension and type flags only - never the file name or path.
398 const char *dot = strrchr(img->filename, '.');
399
400 sentry_value_t o = sentry_value_new_object();
401 sentry_value_set_by_key(o, "extension", sentry_value_new_string(dot ? dot + 1 : ""));
402 sentry_value_set_by_key(o, "pipeline", sentry_value_new_string(pl));
403 sentry_value_set_by_key(o, "raw", sentry_value_new_bool(dt_image_is_raw(img)));
404 sentry_value_set_by_key(o, "ldr", sentry_value_new_bool(dt_image_is_ldr(img)));
405 sentry_value_set_by_key(o, "hdr", sentry_value_new_bool(dt_image_is_hdr(img)));
406 sentry_value_set_by_key(o, "monochrome", sentry_value_new_bool(dt_image_is_monochrome(img)));
407 // dsc.filters != 0 means the buffer still carries a CFA mosaic, i.e. it has not
408 // been demosaiced yet.
409 sentry_value_set_by_key(o, "needs_demosaic", sentry_value_new_bool(img->dsc.filters != 0));
410 sentry_value_set_by_key(o, "width", sentry_value_new_int32(img->width));
411 sentry_value_set_by_key(o, "height", sentry_value_new_int32(img->height));
412 sentry_set_context("processed_image", o);
413}
414
415// Attach OS / hardware context so reports are actionable. No images, files or
416// personal data: only the runtime environment.
417static void _sentry_set_context(void)
418{
419 // Hardware / device context
420 sentry_value_t device = sentry_value_new_object();
421 sentry_value_set_by_key(device, "cpu_logical_cores", sentry_value_new_int32(g_get_num_processors()));
422 sentry_value_set_by_key(device, "openmp_threads", sentry_value_new_int32(dt_get_num_openmp_threads()));
423
424 if(dt_get_total_mem() > 0)
425 {
426 const double mem_gb = (double)dt_get_total_mem() / (1024.0 * 1024.0 * 1024.0);
427 sentry_value_set_by_key(device, "memory_gb", sentry_value_new_double(mem_gb));
428 }
429
430 const gboolean cl_enabled = dt_opencl_is_enabled();
431 sentry_value_set_by_key(device, "opencl_enabled", sentry_value_new_bool(cl_enabled));
432
433#ifdef HAVE_OPENCL
434 // Device enumeration fields (num_devs/dev) only exist in HAVE_OPENCL builds.
436 {
437 sentry_value_t gpus = sentry_value_new_list();
438 for(int i = 0; i < dt_opencl_get_num_devices(); i++)
439 {
440 const char *name = dt_opencl_get_device_name(i);
441 if(name) sentry_value_append(gpus, sentry_value_new_string(name));
442 }
443 sentry_value_set_by_key(device, "opencl_devices", gpus);
444
445 // Tag with the first device so events are filterable by GPU.
446 if(dt_opencl_get_device_name(0)) sentry_set_tag("opencl_device", dt_opencl_get_device_name(0));
447 }
448#endif
449 sentry_set_context("device", device);
450
451#if !defined(_WIN32) && !defined(__APPLE__)
452 // Linux/BSD: display server (X11 vs Wayland) and desktop environment, as
453 // searchable tags. Useful since many GUI bugs are backend/DE specific.
454 const char *session_type = g_getenv("XDG_SESSION_TYPE");
455 if(!session_type || !*session_type)
456 {
457 // Fall back to the well-known display sockets if the session type is unset.
458 if(g_getenv("WAYLAND_DISPLAY"))
459 session_type = "wayland";
460 else if(g_getenv("DISPLAY"))
461 session_type = "x11";
462 }
463 if(session_type && *session_type) sentry_set_tag("display_server", session_type);
464
465 const char *desktop = g_getenv("XDG_CURRENT_DESKTOP");
466 if(!desktop || !*desktop) desktop = g_getenv("DESKTOP_SESSION");
467 if(desktop && *desktop) sentry_set_tag("desktop_environment", desktop);
468
469 // What GTK actually renders on (may differ from the session, e.g. an X11 app
470 // under XWayland). The GObject type name ("GdkWaylandDisplay" / "GdkX11Display")
471 // gives this without pulling in the gdkwayland/gdkx backend headers.
472 GdkDisplay *display = gdk_display_get_default();
473 if(display) sentry_set_tag("gdk_backend", G_OBJECT_TYPE_NAME(display));
474#endif
475
476 // Display scaling and main window geometry (GUI sessions only). DPI/PPD come
477 // from the GUI, already computed during dt_gui_gtk_init(). The window size is
478 // read from conf, which holds the restored/last geometry and is kept up to date
479 // live on every resize - more reliable than the not-yet-mapped window here.
481 {
482 sentry_value_t scr = sentry_value_new_object();
483 sentry_value_set_by_key(scr, "dpi", sentry_value_new_double(dt_screen_dpi()));
484 sentry_value_set_by_key(scr, "dpi_factor", sentry_value_new_double(dt_screen_dpi_factor()));
485 sentry_value_set_by_key(scr, "ppd", sentry_value_new_double(dt_screen_ppd()));
486
487 const int win_w = dt_conf_get_int("ui_last/window_width");
488 const int win_h = dt_conf_get_int("ui_last/window_height");
489 if(win_w > 0 && win_h > 0)
490 {
491 sentry_value_set_by_key(scr, "window_width", sentry_value_new_int32(win_w));
492 sentry_value_set_by_key(scr, "window_height", sentry_value_new_int32(win_h));
493
494 // Searchable tag so issues can be filtered/grouped by window size.
495 char wbuf[32];
496 snprintf(wbuf, sizeof(wbuf), "%dx%d", win_w, win_h);
497 sentry_set_tag("window_size", wbuf);
498 }
499
500 // Monitor resolution (logical pixels) of the primary monitor.
501 GdkDisplay *gdkdisp = gdk_display_get_default();
502 GdkMonitor *mon = gdkdisp ? gdk_display_get_primary_monitor(gdkdisp) : NULL;
503 if(!mon && gdkdisp && gdk_display_get_n_monitors(gdkdisp) > 0)
504 mon = gdk_display_get_monitor(gdkdisp, 0);
505 if(mon)
506 {
507 GdkRectangle geo;
508 gdk_monitor_get_geometry(mon, &geo);
509 sentry_value_set_by_key(scr, "screen_width", sentry_value_new_int32(geo.width));
510 sentry_value_set_by_key(scr, "screen_height", sentry_value_new_int32(geo.height));
511 sentry_value_set_by_key(scr, "monitor_scale_factor",
512 sentry_value_new_int32(gdk_monitor_get_scale_factor(mon)));
513
514 char sbuf[32];
515 snprintf(sbuf, sizeof(sbuf), "%dx%d", geo.width, geo.height);
516 sentry_set_tag("screen_size", sbuf);
517 }
518 sentry_set_context("display", scr);
519 }
520
521 // Build info as searchable tags so crash events can be filtered/grouped immediately.
522 // build_type is the CMake build type (Debug/Release/RelWithDebInfo); in Debug asserts
523 // are active, in Release/RelWithDebInfo NDEBUG is defined and assert() is compiled out.
524 sentry_set_tag("build_type", DT_BUILD_TYPE);
525#ifdef NDEBUG
526 sentry_set_tag("asserts", "disabled");
527#else
528 sentry_set_tag("asserts", "enabled");
529#endif
530 // Full C compiler flags baked in at configure time (includes -DNDEBUG, -O3, -g, etc.)
531 // so it is unambiguous which compilation mode produced the crashing binary.
532 sentry_set_extra("build_cflags", sentry_value_new_string(DT_BUILD_C_FLAGS));
533 sentry_set_tag("opencl", cl_enabled ? "yes" : "no");
534
535 // Distribution channel as a searchable tag (the Sentry environment carries it too, with
536 // the platform appended -- see dt_sentry_init) so the channel alone stays queryable in
537 // dt_sentry_init), so official nightly builds can be told apart from self-builds.
538 sentry_set_tag("build_channel", DT_BUILD_CHANNEL);
539
540 // Stable per-run id, shared with usage analytics (PostHog) so the same session
541 // can be correlated across both systems without double-counting.
542 sentry_set_tag("session_id", dt_session_id());
543
544 // Use the same anonymous per-installation id as PostHog's distinct_id, so the
545 // "users" counted by Sentry de-duplicate against usage analytics.
546 sentry_value_t user = sentry_value_new_object();
547 sentry_value_set_by_key(user, "id", sentry_value_new_string(dt_install_id()));
548 sentry_set_user(user);
549
550 // Human-readable version string (the release itself is the commit SHA). May be a
551 // full "0.0.0+3848~ghash" or, on a shallow clone, just the abbreviated hash.
552 sentry_set_tag("version", darktable_package_version);
553
554 // Surface how many crash-free sessions preceded this one (local mirror of the
555 // server-side release-health metric, useful directly on the event).
556 sentry_set_extra("clean_sessions_local",
557 sentry_value_new_int32(dt_conf_get_int(DT_SENTRY_CLEAN_SESSIONS_KEY)));
558
559 // Length of the previous clean session and cumulative usage time, so events
560 // (e.g. crashes) carry the user's recent/total session history. The current
561 // session's own length is stamped per-event by _sentry_before_send().
562 sentry_set_extra("previous_session_seconds",
563 sentry_value_new_int32(dt_conf_get_int(DT_SENTRY_LAST_SESSION_KEY)));
564 sentry_set_extra("total_session_seconds",
565 sentry_value_new_int64(dt_conf_get_int64(DT_SENTRY_TOTAL_SESSION_KEY)));
566}
567
568// One word for the platform, the same three the website's package tables use.
569static const char *_sentry_platform(void)
570{
571#if defined(_WIN32)
572 return "windows";
573#elif defined(__APPLE__)
574 return "macos";
575#else
576 return "linux";
577#endif
578}
579
580
581void dt_sentry_init(const gboolean have_gui)
582{
583 // Consent is gathered once at startup by dt_privacy_ask_consent() (a single
584 // dialog shared with usage analytics). Here we only honor the resulting toggle.
585 if(!dt_conf_get_bool(DT_SENTRY_ENABLED_KEY))
586 return;
587
588 if(SENTRY_DSN[0] == '\0')
589 {
590 dt_print(DT_DEBUG_CONTROL, "[sentry] no DSN configured, crash reporting disabled\n");
591 return;
592 }
593
594 sentry_options_t *options = sentry_options_new();
595 sentry_options_set_dsn(options, SENTRY_DSN);
596
597 // Keep the crash database next to our other runtime caches.
598 char cachedir[DT_PATH_MAX] = { 0 };
599 dt_loc_get_user_cache_dir(cachedir, sizeof(cachedir));
600 char *db_path = g_build_filename(cachedir, "sentry-native", NULL);
601 sentry_options_set_database_path(options, db_path);
602 g_free(db_path);
603
604 // Release is keyed on the full commit SHA: it is the only build identifier that
605 // is identical across shallow and full clones, so events from official builds and
606 // self-builds of the same commit group into one release (the version string's
607 // abbreviated hash / commit count is not consistent across clone types). The
608 // human-readable version is attached as a tag in _sentry_set_context().
609 char *release = g_strdup_printf("ansel@%s", darktable_commit_hash);
610 sentry_options_set_release(options, release);
611 g_free(release);
612
613 // Environment separates official nightly builds from local/self-builds in the
614 // dashboard and release-health metrics. The compiler/optimization build type is
615 // kept separately as the "build_type" extra.
616 // The environment is the build channel AND the platform, "nightly-windows". Sentry's
617 // sessions API groups crash-free rates by project, release, environment and status
618 // and by nothing else -- no OS, no tag -- so the platform has to ride in here for the
619 // website to show a build's crash rate per package rather than one figure for the
620 // Windows installer, the AppImage and both dmgs alike. Channel first, platform last:
621 // a channel may itself contain hyphens ("package-fedora"), so readers split on the
622 // LAST one. Sessions recorded before this carry the bare channel.
623 char environment[128];
624 snprintf(environment, sizeof(environment), "%s-%s", DT_BUILD_CHANNEL, _sentry_platform());
625 sentry_options_set_environment(options, environment);
626 sentry_options_set_debug(options, (dt_get_debug_flags() & DT_DEBUG_CONTROL) ? 1 : 0);
627
628 // Stamp non-crash events with the session length...
629 sentry_options_set_before_send(options, _sentry_before_send, NULL);
630 // ...and for crashes, stamp the session length and attach a full gdb backtrace
631 // (on_crash replaces before_send for crash events).
632 sentry_options_set_on_crash(options, _sentry_on_crash, NULL);
633
634 // Release health: starts a session now, ended healthy on dt_sentry_shutdown()
635 // or marked crashed by the in-process handler. This is what produces the
636 // "sessions that ended with no error" / crash-free rate metric.
637 sentry_options_set_auto_session_tracking(options, 1);
638
639 if(sentry_init(options) == 0)
640 {
641 _sentry_inited = TRUE;
642 _sentry_set_context();
643 dt_print(DT_DEBUG_CONTROL, "[sentry] crash reporting initialized\n");
644 }
645 else
646 {
647 dt_print(DT_DEBUG_ALWAYS, "[sentry] initialization failed\n");
648 }
649}
650
651void dt_sentry_shutdown(void)
652{
653 if(!_sentry_inited) return;
654
655 // This session ended without a crash: bump the local counter before closing
656 // so the next run (and any future crash) sees the updated count.
657 dt_conf_set_int(DT_SENTRY_CLEAN_SESSIONS_KEY, dt_conf_get_int(DT_SENTRY_CLEAN_SESSIONS_KEY) + 1);
658
659 // Record this healthy session's length (sentry's release health already tracks
660 // the per-session duration server-side; this keeps a local record and feeds the
661 // "previous/total session seconds" context attached on the next run).
662 const int dur = (int)_sentry_session_seconds();
663 dt_conf_set_int(DT_SENTRY_LAST_SESSION_KEY, dur);
664 dt_conf_set_int64(DT_SENTRY_TOTAL_SESSION_KEY,
665 dt_conf_get_int64(DT_SENTRY_TOTAL_SESSION_KEY) + dur);
666
667 sentry_close();
668 _sentry_inited = FALSE;
669
670 if(_module_usage)
671 {
672 g_hash_table_destroy(_module_usage);
673 _module_usage = NULL;
674 }
675}
676
677#else // !HAVE_SENTRY
678
679void dt_sentry_init(const gboolean have_gui)
680{
681}
682
684{
685}
686
688{
689 return FALSE;
690}
691
692void dt_sentry_record_module_usage(const char *category, const char *name)
693{
694}
695
696void dt_sentry_set_processed_image(const struct dt_image_t *img, const char *pipeline)
697{
698}
699
700#endif // HAVE_SENTRY
701
702// clang-format off
703// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
704// vim: shiftwidth=2 expandtab tabstop=2 cindent
705// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
706// clang-format on
const char * dt_install_id(void)
Definition darktable.c:359
const char * dt_session_id(void)
Definition darktable.c:345
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
const float v
const dt_colormatrix_t dt_aligned_pixel_t out
int dt_conf_get_bool(const char *name)
void dt_conf_set_int(const char *name, int val)
void dt_conf_set_int64(const char *name, int64_t val)
int dt_conf_get_int(const char *name)
Integer for name, clamped to the bounds declared in the XML.
int64_t dt_conf_get_int64(const char *name)
64-bit integer for name, clamped to its declared bounds.
gboolean dt_image_is_raw(const dt_image_t *img)
gboolean dt_image_is_hdr(const dt_image_t *img)
gboolean dt_image_is_monochrome(const dt_image_t *img)
gboolean dt_image_is_ldr(const dt_image_t *img)
const char darktable_commit_hash[]
const char darktable_package_string[]
const char darktable_package_version[]
#define DT_BUILD_CHANNEL
#define DT_BUILD_TYPE
#define DT_BUILD_C_FLAGS
int dt_get_num_openmp_threads(void)
Number of OpenMP threads the application decided to use.
Definition darktable.c:518
size_t dt_get_total_mem(void)
Definition darktable.c:2413
void dt_loc_get_user_cache_dir(char *cachedir, size_t bufsize)
@ DT_DEBUG_CONTROL
Definition logging.h:52
@ DT_DEBUG_ALWAYS
Definition logging.h:49
int32_t dt_get_debug_flags(void)
Definition darktable.c:2085
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
float *const restrict const size_t k
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
char * key
const char * dt_opencl_get_device_name(const int devid)
Human-readable device name, owned by the OpenCL module.
Definition opencl.c:2009
int dt_opencl_is_enabled(void)
Definition opencl.c:3262
int dt_opencl_get_num_devices(void)
Number of usable OpenCL devices; 0 when OpenCL is unavailable.
Definition opencl.c:2002
#define DT_PATH_MAX
Buffer size for a filesystem path anywhere in Ansel.
Definition paths.h:57
const char * name
Definition pdf.h:90
double dt_screen_dpi(void)
double dt_screen_dpi_factor(void)
gboolean dt_screen_metrics_probed(void)
double dt_screen_ppd(void)
gboolean dt_sentry_backtrace_captured(void)
Definition sentry.c:687
void dt_sentry_record_module_usage(const char *category, const char *name)
Definition sentry.c:692
void dt_sentry_shutdown(void)
Definition sentry.c:683
void dt_sentry_init(const gboolean have_gui)
Definition sentry.c:679
void dt_sentry_set_processed_image(const struct dt_image_t *img, const char *pipeline)
Definition sentry.c:696
void(* dt_sentry_crash_observer_t)(const char *backtrace, size_t backtrace_len)
Handed the crash backtrace from inside the crash handler.
Definition sentry.h:62
void dt_sentry_add_crash_observer(dt_sentry_crash_observer_t observer)
Register a function to be handed the backtrace when the process crashes.
static const char *const mon[12]
Definition strptime.c:99
int32_t height
Definition image.h:397
int32_t width
Definition image.h:397
dt_iop_buffer_dsc_t dsc
Definition image.h:419
char filename[DT_MAX_FILENAME_LEN]
Definition image.h:386
int32_t id
Definition image.h:401
uint32_t filters
Definition format.h:89
#define PR_SET_PTRACER
typedef double((*spd)(unsigned long int wavelength, double TempK))
double dt_get_start_wtime(void)
Definition darktable.c:2080
static double dt_get_wtime(void)
Definition times.h:43