aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/crash/sentry.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/crash/sentry.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/crash/sentry.zig')
-rw-r--r--vendor/libghostty-vt/src/crash/sentry.zig317
1 files changed, 317 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/crash/sentry.zig b/vendor/libghostty-vt/src/crash/sentry.zig
new file mode 100644
index 00000000..555b70fe
--- /dev/null
+++ b/vendor/libghostty-vt/src/crash/sentry.zig
@@ -0,0 +1,317 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const builtin = @import("builtin");
+const build_config = @import("../build_config.zig");
+const build_options = @import("build_options");
+const sentry = if (build_options.sentry) @import("sentry");
+const internal_os = @import("../os/main.zig");
+const crash = @import("main.zig");
+const state = &@import("../global.zig").state;
+const Surface = @import("../Surface.zig");
+
+const log = std.log.scoped(.sentry);
+
+/// The global state for the Sentry SDK. This is unavoidable since crash
+/// handling is a global process-wide thing.
+var init_thread: ?std.Thread = null;
+
+/// Thread-local state that can be set by thread main functions so that
+/// crashes have more context.
+///
+/// This is a hack over Sentry native SDK limitations. The native SDK has
+/// one global scope for all threads and no support for thread-local scopes.
+/// This means that if we want to set thread-specific data we have to do it
+/// on our own in the on crash callback.
+pub const ThreadState = struct {
+ /// Thread type, used to tag the crash
+ type: Type,
+
+ /// The surface that this thread is attached to.
+ surface: *Surface,
+
+ pub const Type = enum { main, renderer, io };
+};
+
+/// See ThreadState. This should only ever be set by the owner of the
+/// thread entry function.
+pub threadlocal var thread_state: ?ThreadState = null;
+
+/// Process-wide initialization of our Sentry client.
+///
+/// This should only be called from one thread, and deinit should be called
+/// from the same thread that calls init to avoid data races.
+///
+/// PRIVACY NOTE: I want to make it very clear that Ghostty by default does
+/// NOT send any data over the network. We use the Sentry native SDK to collect
+/// crash reports and logs, but we only store them locally (see Transport).
+/// It is up to the user to grab the logs and manually send them to us
+/// (or they own Sentry instance) if they want to.
+pub fn init(gpa: Allocator) !void {
+ if (comptime !build_options.sentry) return;
+
+ // Not supported on Windows currently, doesn't build.
+ if (comptime builtin.os.tag == .windows) return;
+
+ // const start = try std.time.Instant.now();
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // // "[updateFrame critical time] <START us>\t<TIME_TAKEN us>"
+ // std.log.err("[sentry init time] start={}us duration={}ns", .{ start_micro, end.since(start) / std.time.ns_per_us });
+ // }
+
+ // Must only start once
+ assert(init_thread == null);
+
+ // We use a thread for initializing Sentry because initialization takes
+ // ~2k ns on my M3 Max. That's not a LOT of time but it's enough to be
+ // 90% of our pre-App startup time. Everything Sentry is doing initially
+ // is safe to do on a separate thread and fast enough that its very
+ // likely to be done before a crash occurs.
+ const thr = try std.Thread.spawn(
+ .{},
+ initThread,
+ .{gpa},
+ );
+ thr.setName("sentry-init") catch {};
+ init_thread = thr;
+}
+
+fn initThread(gpa: Allocator) !void {
+ if (comptime !build_options.sentry) return;
+
+ // Right now, on Darwin, `std.Thread.setName` can only name the current
+ // thread, and we have no way to get the current thread from within it,
+ // so instead we use this code to name the thread instead.
+ if (builtin.os.tag.isDarwin()) {
+ internal_os.macos.pthread_setname_np(&"sentry-init".*);
+ }
+
+ var arena = std.heap.ArenaAllocator.init(gpa);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ const transport = sentry.Transport.init(&Transport.send);
+ // This will crash if the transport was never used so we avoid
+ // that for now. This probably leaks some memory but it'd be very
+ // small and a one time cost. Once this is fixed upstream we can
+ // remove this.
+ //errdefer transport.deinit();
+
+ const opts = sentry.c.sentry_options_new();
+ errdefer sentry.c.sentry_options_free(opts);
+ sentry.c.sentry_options_set_release_n(
+ opts,
+ build_config.version_string.ptr,
+ build_config.version_string.len,
+ );
+ sentry.c.sentry_options_set_transport(opts, @ptrCast(transport));
+
+ // Set our crash callback. See beforeSend for more details on what we
+ // do here and why we use this.
+ sentry.c.sentry_options_set_before_send(opts, beforeSend, null);
+
+ // Determine the Sentry cache directory.
+ const cache_dir = cache_dir: {
+ // On macOS, we prefer to use the NSCachesDirectory value to be
+ // a more idiomatic macOS application. But if XDG env vars are set
+ // we will respect them.
+ if (comptime builtin.os.tag == .macos) macos: {
+ if (std.posix.getenv("XDG_CACHE_HOME") != null) break :macos;
+ break :cache_dir try internal_os.macos.cacheDir(
+ alloc,
+ "sentry",
+ );
+ }
+
+ break :cache_dir try internal_os.xdg.cache(
+ alloc,
+ .{ .subdir = "ghostty/sentry" },
+ );
+ };
+ sentry.c.sentry_options_set_database_path_n(
+ opts,
+ cache_dir.ptr,
+ cache_dir.len,
+ );
+
+ if (comptime builtin.mode == .Debug) {
+ // Debug logging for Sentry
+ sentry.c.sentry_options_set_debug(opts, @intFromBool(true));
+ }
+
+ // Initialize
+ if (sentry.c.sentry_init(opts) != 0) return error.SentryInitFailed;
+
+ // Setup some basic tags that we always want present
+ sentry.setTag("build-mode", build_config.mode_string);
+ sentry.setTag("app-runtime", @tagName(build_config.app_runtime));
+ sentry.setTag("font-backend", @tagName(build_config.font_backend));
+ sentry.setTag("renderer", @tagName(build_config.renderer));
+
+ // Log some information about sentry
+ log.debug("sentry initialized database={s}", .{cache_dir});
+}
+
+/// Process-wide deinitialization of our Sentry client. This ensures all
+/// our data is flushed.
+pub fn deinit() void {
+ if (comptime !build_options.sentry) return;
+
+ if (comptime builtin.os.tag == .windows) return;
+
+ // If we're still initializing then wait for init to finish. This
+ // is highly unlikely since init is a very fast operation but we want
+ // to avoid the possibility.
+ const thr = init_thread orelse return;
+ thr.join();
+ _ = sentry.c.sentry_close();
+}
+
+fn beforeSend(
+ event_val: sentry.c.sentry_value_t,
+ _: ?*anyopaque,
+ _: ?*anyopaque,
+) callconv(.c) sentry.c.sentry_value_t {
+ // The native SDK at the time of writing doesn't support thread-local
+ // scopes. The full SDK has one global scope. So we use the beforeSend
+ // handler to set thread-specific data such as window size, grid size,
+ // etc. that we can use to debug crashes.
+
+ // If we don't have thread state we can't reliably determine
+ // metadata such as surface dimensions. In the future we can probably
+ // drop full app state (all surfaces, all windows, etc.).
+ const thr_state = thread_state orelse {
+ log.debug("no thread state, skipping crash metadata", .{});
+ return event_val;
+ };
+
+ // Get our event contexts. At this point Sentry has already merged
+ // all the contexts so we should have this key. If not, we create it.
+ const event: sentry.Value = .{ .value = event_val };
+ const contexts = event.get("contexts") orelse contexts: {
+ const obj = sentry.Value.initObject();
+ event.set("contexts", obj);
+ break :contexts obj;
+ };
+ const tags = event.get("tags") orelse tags: {
+ const obj = sentry.Value.initObject();
+ event.set("tags", obj);
+ break :tags obj;
+ };
+
+ // Store our thread type
+ tags.set("thread-type", sentry.Value.initString(@tagName(thr_state.type)));
+
+ // Read the surface data. This is likely unsafe because on a crash
+ // other threads can continue running. We don't have race-safe way to
+ // access this data so this might be corrupted but it's most likely fine.
+ {
+ const obj = sentry.Value.initObject();
+ errdefer obj.decref();
+ const surface = thr_state.surface;
+ const grid_size = surface.size.grid();
+ obj.set(
+ "screen-width",
+ sentry.Value.initInt32(std.math.cast(i32, surface.size.screen.width) orelse -1),
+ );
+ obj.set(
+ "screen-height",
+ sentry.Value.initInt32(std.math.cast(i32, surface.size.screen.height) orelse -1),
+ );
+ obj.set(
+ "grid-columns",
+ sentry.Value.initInt32(std.math.cast(i32, grid_size.columns) orelse -1),
+ );
+ obj.set(
+ "grid-rows",
+ sentry.Value.initInt32(std.math.cast(i32, grid_size.rows) orelse -1),
+ );
+ obj.set(
+ "cell-width",
+ sentry.Value.initInt32(std.math.cast(i32, surface.size.cell.width) orelse -1),
+ );
+ obj.set(
+ "cell-height",
+ sentry.Value.initInt32(std.math.cast(i32, surface.size.cell.height) orelse -1),
+ );
+
+ contexts.set("Dimensions", obj);
+ }
+
+ return event_val;
+}
+
+pub const Transport = struct {
+ pub fn send(envelope: *sentry.Envelope, ud: ?*anyopaque) callconv(.c) void {
+ _ = ud;
+ defer envelope.deinit();
+
+ // Call our internal impl. If it fails there is nothing we can do
+ // but log to the user.
+ sendInternal(envelope) catch |err| {
+ log.warn("failed to persist crash report err={}", .{err});
+ };
+ }
+
+ /// Implementation of send but we can use Zig errors.
+ fn sendInternal(envelope: *sentry.Envelope) !void {
+ var arena = std.heap.ArenaAllocator.init(state.alloc);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Parse into an envelope structure
+ const json = envelope.serialize();
+ defer sentry.free(@ptrCast(json.ptr));
+ var parsed: crash.Envelope = parsed: {
+ var reader: std.Io.Reader = .fixed(json);
+ break :parsed try crash.Envelope.parse(alloc, &reader);
+ };
+ defer parsed.deinit();
+
+ // If our envelope doesn't have an event then we don't do anything.
+ // To figure this out we first encode it into a string, parse it,
+ // and check if it has an event. Kind of wasteful but the best
+ // option we have at the time of writing this since the C API doesn't
+ // expose this information.
+ if (try shouldDiscard(&parsed)) {
+ log.info("sentry envelope does not contain crash, discarding", .{});
+ return;
+ }
+
+ // Generate a UUID for this envelope. The envelope DOES have an event_id
+ // header but I don't think there is any public API way to get it
+ // afaict so we generate a new UUID for the filename just so we don't
+ // conflict.
+ const uuid = sentry.UUID.init();
+
+ // Get our XDG state directory where we'll store the crash reports.
+ // This directory must exist for writing to work.
+ const dir = try crash.defaultDir(alloc);
+ try std.fs.cwd().makePath(dir.path);
+
+ // Build our final path and write to it.
+ const path = try std.fs.path.join(alloc, &.{
+ dir.path,
+ try std.fmt.allocPrint(alloc, "{s}.ghosttycrash", .{uuid.string()}),
+ });
+ const file = try std.fs.cwd().createFile(path, .{});
+ defer file.close();
+ var buf: [4096]u8 = undefined;
+ var file_writer = file.writer(&buf);
+ try file_writer.interface.writeAll(json);
+ try file_writer.end();
+
+ log.warn("crash report written to disk path={s}", .{path});
+ }
+
+ fn shouldDiscard(envelope: *const crash.Envelope) !bool {
+ // If we have an event item then we're good.
+ for (envelope.items.items) |item| {
+ if (item.itemType() == .event) return false;
+ }
+
+ return true;
+ }
+};