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#endif
22
23#include "common/sentry.h"
24#include "common/darktable.h"
25
26#ifdef HAVE_SENTRY
27
29#include "common/image.h"
30#include "common/opencl.h"
31#include "control/conf.h"
32#include "gui/gtk.h"
33
34#include <sentry.h>
35
36#include <signal.h> // for sig_atomic_t
37#include <string.h> // for strrchr, strcmp
38
39#if defined(__linux__)
40#include <fcntl.h> // for open flags
41#include <sys/prctl.h> // for PR_SET_PTRACER
42#include <sys/wait.h> // for waitpid
43#include <unistd.h> // for fork, getpid
44#endif
45
46#ifndef SENTRY_DSN
47#define SENTRY_DSN ""
48#endif
49
50// Conf keys. "sentry/enabled" is a confgen key (shown in Preferences); the two
51// below are intentionally NOT in confgen so dt_conf_key_exists() reflects whether
52// the user has actually decided / how many clean sessions were recorded.
53#define DT_SENTRY_ENABLED_KEY "sentry/enabled"
54#define DT_SENTRY_ASKED_KEY "sentry/consent_asked"
55#define DT_SENTRY_CLEAN_SESSIONS_KEY "sentry/clean_sessions"
56#define DT_SENTRY_LAST_SESSION_KEY "sentry/last_session_seconds"
57#define DT_SENTRY_TOTAL_SESSION_KEY "sentry/total_session_seconds"
58
59static gboolean _sentry_inited = FALSE;
60
61// Set once sentry's on_crash hook has captured a gdb backtrace, so the local
62// signal handler can skip running gdb a second time for the same crash.
63static volatile sig_atomic_t _sentry_backtrace_captured = 0;
64
65// Per-session module usage counts ("category/name" -> count). Mutated only from
66// the GUI thread; mirrored into the sentry scope on each change so the crash
67// handler never has to read this table (which would be unsafe in a signal context).
68static GHashTable *_module_usage = NULL;
69
70// Dedup state for the currently-processed image. Pipelines run on worker threads
71// (darkroom full/preview, export), so this is guarded by a mutex.
72static GMutex _processed_image_lock;
73static int32_t _processed_imgid = -1;
74static char _processed_pipeline[32] = { 0 };
75
76// Length of the running session, in seconds. darktable.start_wtime is stamped at
77// the very start of dt_init().
78static double _sentry_session_seconds(void)
79{
80 const double dur = dt_get_wtime() - darktable.start_wtime;
81 return (dur > 0.0) ? dur : 0.0;
82}
83
84// Stamp the event with the current session length, in seconds. For crashes this
85// runs inside the crashing process, so the value is the exact time-to-crash.
86static void _sentry_stamp_session_length(sentry_value_t event)
87{
88 const double dur = _sentry_session_seconds();
89
90 // Numeric value under "extra" for inspection.
91 sentry_value_t extra = sentry_value_get_by_key(event, "extra");
92 if(sentry_value_is_null(extra))
93 {
94 extra = sentry_value_new_object();
95 sentry_value_set_by_key(extra, "session_seconds", sentry_value_new_double(dur));
96 sentry_value_set_by_key(event, "extra", extra);
97 }
98 else
99 {
100 sentry_value_set_by_key(extra, "session_seconds", sentry_value_new_double(dur));
101 }
102
103 // String tag so events are searchable/groupable by session length.
104 char buf[32];
105 snprintf(buf, sizeof(buf), "%.0f", dur);
106 sentry_value_t tags = sentry_value_get_by_key(event, "tags");
107 if(sentry_value_is_null(tags))
108 {
109 tags = sentry_value_new_object();
110 sentry_value_set_by_key(tags, "session_seconds", sentry_value_new_string(buf));
111 sentry_value_set_by_key(event, "tags", tags);
112 }
113 else
114 {
115 sentry_value_set_by_key(tags, "session_seconds", sentry_value_new_string(buf));
116 }
117}
118
119// before_send handles NON-crash events (on_crash takes over for crashes). Stamp
120// the session length so every event carries it.
121static sentry_value_t _sentry_before_send(sentry_value_t event, void *hint, void *user_data)
122{
123 _sentry_stamp_session_length(event);
124 return event;
125}
126
127#if defined(__linux__)
128// Run gdb against the crashing process and capture its backtrace into a freshly
129// allocated buffer (NUL-terminated; *len excludes the terminator). Returns NULL
130// on failure. This mirrors the local gdb fallback in system_signal_handling.c but
131// returns the text so it can be attached to the Sentry crash report.
132static char *_sentry_capture_gdb_backtrace(gsize *len)
133{
134 gchar *name = NULL;
135 const int fd = g_file_open_tmp("ansel_sentry_bt_XXXXXX.txt", &name, NULL);
136 if(fd == -1) return NULL;
137 close(fd);
138
139 gchar *pid_arg = g_strdup_printf("%d", (int)getpid());
140 gchar *exe_arg = g_strdup_printf("/proc/%s/exe", pid_arg);
141 gchar *log_file_arg = g_strdup_printf("set logging file %s", name);
142
143 char *contents = NULL;
144 const pid_t pid = fork();
145 if(pid == 0)
146 {
147 // child: gdb attaches to the parent and dumps all threads' backtraces
148 execlp("gdb", "gdb", exe_arg, pid_arg, "-batch",
149 "-ex", "set pagination off",
150 "-ex", "set confirm off",
151 "-ex", log_file_arg,
152 "-ex", "set logging overwrite on",
153 "-ex", "set logging redirect on",
154 "-ex", "set logging enabled on",
155 "-ex", "thread apply all bt full",
156 NULL);
157 _exit(127); // execlp only returns on failure
158 }
159 else if(pid > 0)
160 {
161 prctl(PR_SET_PTRACER, pid, 0, 0, 0); // let the child ptrace us (Yama)
162 waitpid(pid, NULL, 0);
163
164 gsize n = 0;
165 if(g_file_get_contents(name, &contents, &n, NULL))
166 {
167 if(len) *len = n;
168 }
169 else
170 {
171 contents = NULL;
172 }
173 }
174
175 g_unlink(name);
176 g_free(name);
177 g_free(pid_arg);
178 g_free(exe_arg);
179 g_free(log_file_arg);
180 return contents;
181}
182#endif // __linux__
183
184// on_crash replaces before_send for crash events (inproc). It runs before the
185// crash event/attachments are serialized, so this is where we both stamp the
186// session length and attach a full gdb backtrace to the report.
187static sentry_value_t _sentry_on_crash(const sentry_ucontext_t *uctx, sentry_value_t event, void *user_data)
188{
189 _sentry_stamp_session_length(event);
190
191#if defined(__linux__)
192 gsize bt_len = 0;
193 char *bt = _sentry_capture_gdb_backtrace(&bt_len);
194 if(bt && bt_len > 0)
195 {
196 // Registered on the scope, this is picked up when the crash envelope is
197 // assembled (right after this hook returns). sentry copies the bytes.
198 sentry_attach_bytes(bt, bt_len, "gdb-backtrace.txt");
199
200 // Tell the local signal handler (which runs next in the chain) not to run
201 // gdb again for this same crash.
202 _sentry_backtrace_captured = 1;
203 }
204 g_free(bt);
205#endif
206
207 return event;
208}
209
210gboolean dt_sentry_backtrace_captured(void)
211{
212 return _sentry_backtrace_captured != 0;
213}
214
215void dt_sentry_record_module_usage(const char *category, const char *name)
216{
217 if(!_sentry_inited || !category || !name || !*name) return;
218
219 if(!_module_usage)
220 _module_usage = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
221
222 // g_hash_table_insert frees the duplicate key when the entry already exists,
223 // so the table keeps a single owned copy of each key.
224 char *key = g_strdup_printf("%s/%s", category, name);
225 const int count = GPOINTER_TO_INT(g_hash_table_lookup(_module_usage, key)) + 1;
226 g_hash_table_insert(_module_usage, key, GINT_TO_POINTER(count));
227
228 // Push the whole map into the scope as the "module_usage" context. Cheap at
229 // human interaction rates, and keeps the crash path free of table iteration.
230 sentry_value_t obj = sentry_value_new_object();
231 GHashTableIter iter;
232 gpointer k, v;
233 g_hash_table_iter_init(&iter, _module_usage);
234 while(g_hash_table_iter_next(&iter, &k, &v))
235 sentry_value_set_by_key(obj, (const char *)k, sentry_value_new_int32(GPOINTER_TO_INT(v)));
236 sentry_set_context("module_usage", obj);
237}
238
239void dt_sentry_set_processed_image(const struct dt_image_t *img, const char *pipeline)
240{
241 if(!_sentry_inited || !img) return;
242 const char *pl = pipeline ? pipeline : "";
243
244 // Skip the (frequent) case where the same image keeps being reprocessed by the
245 // same pipeline; only push a new context when something actually changed.
246 g_mutex_lock(&_processed_image_lock);
247 if(img->id == _processed_imgid && !strcmp(pl, _processed_pipeline))
248 {
249 g_mutex_unlock(&_processed_image_lock);
250 return;
251 }
252 _processed_imgid = img->id;
253 g_strlcpy(_processed_pipeline, pl, sizeof(_processed_pipeline));
254 g_mutex_unlock(&_processed_image_lock);
255
256 // Extension and type flags only - never the file name or path.
257 const char *dot = strrchr(img->filename, '.');
258
259 sentry_value_t o = sentry_value_new_object();
260 sentry_value_set_by_key(o, "extension", sentry_value_new_string(dot ? dot + 1 : ""));
261 sentry_value_set_by_key(o, "pipeline", sentry_value_new_string(pl));
262 sentry_value_set_by_key(o, "raw", sentry_value_new_bool(dt_image_is_raw(img)));
263 sentry_value_set_by_key(o, "ldr", sentry_value_new_bool(dt_image_is_ldr(img)));
264 sentry_value_set_by_key(o, "hdr", sentry_value_new_bool(dt_image_is_hdr(img)));
265 sentry_value_set_by_key(o, "monochrome", sentry_value_new_bool(dt_image_is_monochrome(img)));
266 // dsc.filters != 0 means the buffer still carries a CFA mosaic, i.e. it has not
267 // been demosaiced yet.
268 sentry_value_set_by_key(o, "needs_demosaic", sentry_value_new_bool(img->dsc.filters != 0));
269 sentry_value_set_by_key(o, "width", sentry_value_new_int32(img->width));
270 sentry_value_set_by_key(o, "height", sentry_value_new_int32(img->height));
271 sentry_set_context("processed_image", o);
272}
273
274// Attach OS / hardware context so reports are actionable. No images, files or
275// personal data: only the runtime environment.
276static void _sentry_set_context(void)
277{
278 // Hardware / device context
279 sentry_value_t device = sentry_value_new_object();
280 sentry_value_set_by_key(device, "cpu_logical_cores", sentry_value_new_int32(g_get_num_processors()));
281 sentry_value_set_by_key(device, "openmp_threads", sentry_value_new_int32(darktable.num_openmp_threads));
282
284 {
285 const double mem_gb = (double)darktable.dtresources.total_memory / (1024.0 * 1024.0 * 1024.0);
286 sentry_value_set_by_key(device, "memory_gb", sentry_value_new_double(mem_gb));
287 }
288
289 const gboolean cl_enabled = dt_opencl_is_enabled();
290 sentry_value_set_by_key(device, "opencl_enabled", sentry_value_new_bool(cl_enabled));
291
292#ifdef HAVE_OPENCL
293 // Device enumeration fields (num_devs/dev) only exist in HAVE_OPENCL builds.
295 {
296 sentry_value_t gpus = sentry_value_new_list();
297 for(int i = 0; i < darktable.opencl->num_devs; i++)
298 {
299 const char *name = darktable.opencl->dev[i].name;
300 if(name) sentry_value_append(gpus, sentry_value_new_string(name));
301 }
302 sentry_value_set_by_key(device, "opencl_devices", gpus);
303
304 // Tag with the first device so events are filterable by GPU.
305 if(darktable.opencl->dev[0].name) sentry_set_tag("opencl_device", darktable.opencl->dev[0].name);
306 }
307#endif
308 sentry_set_context("device", device);
309
310#if !defined(_WIN32) && !defined(__APPLE__)
311 // Linux/BSD: display server (X11 vs Wayland) and desktop environment, as
312 // searchable tags. Useful since many GUI bugs are backend/DE specific.
313 const char *session_type = g_getenv("XDG_SESSION_TYPE");
314 if(!session_type || !*session_type)
315 {
316 // Fall back to the well-known display sockets if the session type is unset.
317 if(g_getenv("WAYLAND_DISPLAY"))
318 session_type = "wayland";
319 else if(g_getenv("DISPLAY"))
320 session_type = "x11";
321 }
322 if(session_type && *session_type) sentry_set_tag("display_server", session_type);
323
324 const char *desktop = g_getenv("XDG_CURRENT_DESKTOP");
325 if(!desktop || !*desktop) desktop = g_getenv("DESKTOP_SESSION");
326 if(desktop && *desktop) sentry_set_tag("desktop_environment", desktop);
327
328 // What GTK actually renders on (may differ from the session, e.g. an X11 app
329 // under XWayland). The GObject type name ("GdkWaylandDisplay" / "GdkX11Display")
330 // gives this without pulling in the gdkwayland/gdkx backend headers.
331 GdkDisplay *display = gdk_display_get_default();
332 if(display) sentry_set_tag("gdk_backend", G_OBJECT_TYPE_NAME(display));
333#endif
334
335 // Display scaling and main window geometry (GUI sessions only). DPI/PPD come
336 // from the GUI, already computed during dt_gui_gtk_init(). The window size is
337 // read from conf, which holds the restored/last geometry and is kept up to date
338 // live on every resize - more reliable than the not-yet-mapped window here.
339 if(darktable.gui)
340 {
341 sentry_value_t scr = sentry_value_new_object();
342 sentry_value_set_by_key(scr, "dpi", sentry_value_new_double(darktable.gui->dpi));
343 sentry_value_set_by_key(scr, "dpi_factor", sentry_value_new_double(darktable.gui->dpi_factor));
344 sentry_value_set_by_key(scr, "ppd", sentry_value_new_double(darktable.gui->ppd));
345
346 const int win_w = dt_conf_get_int("ui_last/window_width");
347 const int win_h = dt_conf_get_int("ui_last/window_height");
348 if(win_w > 0 && win_h > 0)
349 {
350 sentry_value_set_by_key(scr, "window_width", sentry_value_new_int32(win_w));
351 sentry_value_set_by_key(scr, "window_height", sentry_value_new_int32(win_h));
352
353 // Searchable tag so issues can be filtered/grouped by window size.
354 char wbuf[32];
355 snprintf(wbuf, sizeof(wbuf), "%dx%d", win_w, win_h);
356 sentry_set_tag("window_size", wbuf);
357 }
358
359 // Monitor resolution (logical pixels) of the primary monitor.
360 GdkDisplay *gdkdisp = gdk_display_get_default();
361 GdkMonitor *mon = gdkdisp ? gdk_display_get_primary_monitor(gdkdisp) : NULL;
362 if(!mon && gdkdisp && gdk_display_get_n_monitors(gdkdisp) > 0)
363 mon = gdk_display_get_monitor(gdkdisp, 0);
364 if(mon)
365 {
366 GdkRectangle geo;
367 gdk_monitor_get_geometry(mon, &geo);
368 sentry_value_set_by_key(scr, "screen_width", sentry_value_new_int32(geo.width));
369 sentry_value_set_by_key(scr, "screen_height", sentry_value_new_int32(geo.height));
370 sentry_value_set_by_key(scr, "monitor_scale_factor",
371 sentry_value_new_int32(gdk_monitor_get_scale_factor(mon)));
372
373 char sbuf[32];
374 snprintf(sbuf, sizeof(sbuf), "%dx%d", geo.width, geo.height);
375 sentry_set_tag("screen_size", sbuf);
376 }
377 sentry_set_context("display", scr);
378 }
379
380 // Build / runtime info as searchable extras
381 sentry_set_extra("build_type", sentry_value_new_string(DT_BUILD_TYPE));
382 sentry_set_tag("opencl", cl_enabled ? "yes" : "no");
383
384 // Surface how many crash-free sessions preceded this one (local mirror of the
385 // server-side release-health metric, useful directly on the event).
386 sentry_set_extra("clean_sessions_local",
387 sentry_value_new_int32(dt_conf_get_int(DT_SENTRY_CLEAN_SESSIONS_KEY)));
388
389 // Length of the previous clean session and cumulative usage time, so events
390 // (e.g. crashes) carry the user's recent/total session history. The current
391 // session's own length is stamped per-event by _sentry_before_send().
392 sentry_set_extra("previous_session_seconds",
393 sentry_value_new_int32(dt_conf_get_int(DT_SENTRY_LAST_SESSION_KEY)));
394 sentry_set_extra("total_session_seconds",
395 sentry_value_new_int64(dt_conf_get_int64(DT_SENTRY_TOTAL_SESSION_KEY)));
396}
397
398void dt_sentry_init(const gboolean have_gui)
399{
400 // Consent is gathered once at startup by dt_privacy_ask_consent() (a single
401 // dialog shared with usage analytics). Here we only honor the resulting toggle.
402 if(!dt_conf_get_bool(DT_SENTRY_ENABLED_KEY))
403 return;
404
405 if(SENTRY_DSN[0] == '\0')
406 {
407 dt_print(DT_DEBUG_CONTROL, "[sentry] no DSN configured, crash reporting disabled\n");
408 return;
409 }
410
411 sentry_options_t *options = sentry_options_new();
412 sentry_options_set_dsn(options, SENTRY_DSN);
413
414 // Keep the crash database next to our other runtime caches.
415 char cachedir[PATH_MAX] = { 0 };
416 dt_loc_get_user_cache_dir(cachedir, sizeof(cachedir));
417 char *db_path = g_build_filename(cachedir, "sentry-native", NULL);
418 sentry_options_set_database_path(options, db_path);
419 g_free(db_path);
420
421 char *release = g_strdup_printf("ansel@%s", darktable_package_version);
422 sentry_options_set_release(options, release);
423 g_free(release);
424
425 sentry_options_set_environment(options, DT_BUILD_TYPE);
426 sentry_options_set_debug(options, (darktable.unmuted & DT_DEBUG_CONTROL) ? 1 : 0);
427
428 // Stamp non-crash events with the session length...
429 sentry_options_set_before_send(options, _sentry_before_send, NULL);
430 // ...and for crashes, stamp the session length and attach a full gdb backtrace
431 // (on_crash replaces before_send for crash events).
432 sentry_options_set_on_crash(options, _sentry_on_crash, NULL);
433
434 // Release health: starts a session now, ended healthy on dt_sentry_shutdown()
435 // or marked crashed by the in-process handler. This is what produces the
436 // "sessions that ended with no error" / crash-free rate metric.
437 sentry_options_set_auto_session_tracking(options, 1);
438
439 if(sentry_init(options) == 0)
440 {
441 _sentry_inited = TRUE;
442 _sentry_set_context();
443 dt_print(DT_DEBUG_CONTROL, "[sentry] crash reporting initialized\n");
444 }
445 else
446 {
447 dt_print(DT_DEBUG_ALWAYS, "[sentry] initialization failed\n");
448 }
449}
450
451void dt_sentry_shutdown(void)
452{
453 if(!_sentry_inited) return;
454
455 // This session ended without a crash: bump the local counter before closing
456 // so the next run (and any future crash) sees the updated count.
457 dt_conf_set_int(DT_SENTRY_CLEAN_SESSIONS_KEY, dt_conf_get_int(DT_SENTRY_CLEAN_SESSIONS_KEY) + 1);
458
459 // Record this healthy session's length (sentry's release health already tracks
460 // the per-session duration server-side; this keeps a local record and feeds the
461 // "previous/total session seconds" context attached on the next run).
462 const int dur = (int)_sentry_session_seconds();
463 dt_conf_set_int(DT_SENTRY_LAST_SESSION_KEY, dur);
464 dt_conf_set_int64(DT_SENTRY_TOTAL_SESSION_KEY,
465 dt_conf_get_int64(DT_SENTRY_TOTAL_SESSION_KEY) + dur);
466
467 sentry_close();
468 _sentry_inited = FALSE;
469
470 if(_module_usage)
471 {
472 g_hash_table_destroy(_module_usage);
473 _module_usage = NULL;
474 }
475}
476
477#else // !HAVE_SENTRY
478
479void dt_sentry_init(const gboolean have_gui)
480{
481}
482
484{
485}
486
488{
489 return FALSE;
490}
491
492void dt_sentry_record_module_usage(const char *category, const char *name)
493{
494}
495
496void dt_sentry_set_processed_image(const struct dt_image_t *img, const char *pipeline)
497{
498}
499
500#endif // HAVE_SENTRY
501
502// clang-format off
503// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
504// vim: shiftwidth=2 expandtab tabstop=2 cindent
505// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
506// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
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)
char * key
char * name
const char darktable_package_version[]
#define DT_BUILD_TYPE
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)
int64_t dt_conf_get_int64(const char *name)
darktable_t darktable
Definition darktable.c:181
void dt_print(dt_debug_thread_t thread, const char *msg,...)
Definition darktable.c:1542
@ DT_DEBUG_CONTROL
Definition darktable.h:716
@ DT_DEBUG_ALWAYS
Definition darktable.h:713
static double dt_get_wtime(void)
Definition darktable.h:914
#define PATH_MAX
Definition darktable.h:1062
void dt_loc_get_user_cache_dir(char *cachedir, size_t bufsize)
const float v
float *const restrict const size_t k
int dt_opencl_is_enabled(void)
Definition opencl.c:2737
gboolean dt_sentry_backtrace_captured(void)
Definition sentry.c:487
void dt_sentry_record_module_usage(const char *category, const char *name)
Definition sentry.c:492
void dt_sentry_shutdown(void)
Definition sentry.c:483
void dt_sentry_init(const gboolean have_gui)
Definition sentry.c:479
void dt_sentry_set_processed_image(const struct dt_image_t *img, const char *pipeline)
Definition sentry.c:496
static const char *const mon[12]
Definition strptime.c:99
int32_t num_openmp_threads
Definition darktable.h:758
struct dt_gui_gtk_t * gui
Definition darktable.h:775
struct dt_sys_resources_t dtresources
Definition darktable.h:834
struct dt_opencl_t * opencl
Definition darktable.h:785
int32_t unmuted
Definition darktable.h:760
double start_wtime
Definition darktable.h:828
double dpi
Definition gtk.h:200
double ppd
Definition gtk.h:200
double dpi_factor
Definition gtk.h:200
int32_t height
Definition image.h:315
int32_t width
Definition image.h:315
dt_iop_buffer_dsc_t dsc
Definition image.h:337
char filename[DT_MAX_FILENAME_LEN]
Definition image.h:304
int32_t id
Definition image.h:319
uint32_t filters
Definition format.h:60
const char * name
Definition opencl.h:149
int num_devs
Definition opencl.h:236
dt_opencl_device_t * dev
Definition opencl.h:246
int inited
Definition opencl.h:232
typedef double((*spd)(unsigned long int wavelength, double TempK))