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