|
| 1 | +const std = @import("std"); |
| 2 | +const builtin = @import("builtin"); |
| 3 | + |
| 4 | +const Allocator = std.mem.Allocator; |
| 5 | +const uuidv4 = @import("../id.zig").uuidv4; |
| 6 | + |
| 7 | +const log = std.log.scoped(.telemetry); |
| 8 | + |
| 9 | +const BATCH_SIZE = 5; |
| 10 | +const BATCH_END = BATCH_SIZE - 1; |
| 11 | +const ID_FILE = "lightpanda.id"; |
| 12 | + |
| 13 | +pub const Telemetry = TelemetryT(blk: { |
| 14 | + if (builtin.mode == .Debug or builtin.is_test) break :blk NoopProvider; |
| 15 | + break :blk @import("ligtpanda.zig").Lightpanda; |
| 16 | +}); |
| 17 | + |
| 18 | +fn TelemetryT(comptime P: type) type { |
| 19 | + return struct { |
| 20 | + // an "install" id that we [try to] persist and re-use between runs |
| 21 | + // null on IO error |
| 22 | + iid: ?[36]u8, |
| 23 | + |
| 24 | + // a "execution" id is an id that represents this specific run |
| 25 | + eid: [36]u8, |
| 26 | + provider: P, |
| 27 | + |
| 28 | + // batch of events, pending[0..count] are pending |
| 29 | + pending: [BATCH_SIZE]Event, |
| 30 | + count: usize, |
| 31 | + disabled: bool, |
| 32 | + |
| 33 | + const Self = @This(); |
| 34 | + |
| 35 | + pub fn init(allocator: Allocator) Self { |
| 36 | + const disabled = std.process.hasEnvVarConstant("LIGHTPANDA_DISABLE_TELEMETRY"); |
| 37 | + |
| 38 | + var eid: [36]u8 = undefined; |
| 39 | + uuidv4(&eid) |
| 40 | + |
| 41 | + return .{ |
| 42 | + .eid = eid, |
| 43 | + .iid = if (disabled) null else getOrCreateId(), |
| 44 | + .disabled = disabled, |
| 45 | + .provider = try P.init(allocator), |
| 46 | + }; |
| 47 | + } |
| 48 | + |
| 49 | + pub fn deinit(self: *Self) void { |
| 50 | + self.provider.deinit(); |
| 51 | + } |
| 52 | + |
| 53 | + pub fn record(self: *Self, event: Event) void { |
| 54 | + if (self.disabled) { |
| 55 | + return; |
| 56 | + } |
| 57 | + |
| 58 | + const count = self.count; |
| 59 | + self.pending[count] = event; |
| 60 | + if (count < BATCH_END) { |
| 61 | + self.count = count + 1; |
| 62 | + return; |
| 63 | + } |
| 64 | + |
| 65 | + const iid = if (self.iid) |*iid| *iid else null; |
| 66 | + self.provider.send(iid, &self.eid, &self.pending) catch |err| { |
| 67 | + log.warn("failed to record event: {}", .{err}); |
| 68 | + }; |
| 69 | + self.count = 0; |
| 70 | + } |
| 71 | + }; |
| 72 | +} |
| 73 | + |
| 74 | +fn getOrCreateId() ?[36]u8 { |
| 75 | + var buf: [37]u8 = undefined; |
| 76 | + const data = std.fs.cwd().readFile(ID_FILE, &buf) catch |err| switch (err) blk: { |
| 77 | + error.FileNotFound => break :bkl &.{}, |
| 78 | + else => { |
| 79 | + log.warn("failed to open id file: {}", .{err}); |
| 80 | + return null, |
| 81 | + }, |
| 82 | + } |
| 83 | + |
| 84 | + var id: [36]u8 = undefined; |
| 85 | + if (data.len == 36) { |
| 86 | + @memcpy(id[0..36], data) |
| 87 | + return id; |
| 88 | + } |
| 89 | + |
| 90 | + uuidv4(&id); |
| 91 | + std.fs.cwd().writeFile(.{.sub_path = ID_FILE, .data = buf[0..36]}) catch |err| { |
| 92 | + log.warn("failed to write to id file: {}", .{err}); |
| 93 | + return null; |
| 94 | + }; |
| 95 | + return id; |
| 96 | +} |
| 97 | + |
| 98 | +pub const Event = union(enum) { |
| 99 | + run: Run, |
| 100 | + |
| 101 | + const Run = struct { |
| 102 | + version: []const u8, |
| 103 | + mode: RunMode, |
| 104 | + |
| 105 | + const RunMode = enum { |
| 106 | + fetch, |
| 107 | + serve, |
| 108 | + }; |
| 109 | + }; |
| 110 | +}; |
| 111 | + |
| 112 | +const NoopProvider = struct { |
| 113 | + fn init(_: Allocator) !NoopProvider { |
| 114 | + return .{}; |
| 115 | + } |
| 116 | + fn deinit(_: NoopProvider) void {} |
| 117 | + pub fn record(_: NoopProvider, _: Event) !void {} |
| 118 | +}; |
| 119 | + |
| 120 | +extern fn setenv(name: [*:0]u8, value: [*:0]u8, override: c_int) c_int; |
| 121 | +extern fn unsetenv(name: [*:0]u8) c_int; |
| 122 | +const testing = std.testing; |
| 123 | +test "telemetry: disabled by environment" { |
| 124 | + _ = setenv(@constCast("LIGHTPANDA_DISABLE_TELEMETRY"), @constCast(""), 0); |
| 125 | + defer _ = unsetenv(@constCast("LIGHTPANDA_DISABLE_TELEMETRY")); |
| 126 | + |
| 127 | + const FailingProvider = struct { |
| 128 | + fn init(_: Allocator) !@This() { |
| 129 | + return .{}; |
| 130 | + } |
| 131 | + fn deinit(_: @This()) void {} |
| 132 | + pub fn record(_: @This(), _: Event) !void { |
| 133 | + unreachable; |
| 134 | + } |
| 135 | + }; |
| 136 | + |
| 137 | + var telemetry = TelemetryT(FailingProvider).init(testing.allocator); |
| 138 | + defer telemetry.deinit(); |
| 139 | + telemetry.record(.{ .run = .{ .mode = .serve, .version = "123" } }); |
| 140 | +} |
0 commit comments