Simple analytics

This commit is contained in:
Jarred Sumner
2021-10-05 02:27:49 -07:00
parent d2be50bf4d
commit 00e7b7c3d5
8 changed files with 1070 additions and 1 deletions

View File

@@ -68,7 +68,7 @@ endif
bun: vendor build-obj bun-link-lld-release
vendor-without-check: api node-fallbacks runtime_js fallback_decoder bun_error mimalloc picohttp
vendor-without-check: api analytics node-fallbacks runtime_js fallback_decoder bun_error mimalloc picohttp
vendor: require init-submodules vendor-without-check
@@ -380,3 +380,6 @@ sizegen:
picohttp:
$(CC) -O3 -g -fPIE -c src/deps/picohttpparser.c -Isrc/deps -o src/deps/picohttpparser.o; cd ../../
analytics:
./node_modules/.bin/peechy --schema src/analytics/schema.peechy --go src/analytics/analytics.go --zig src/analytics/analytics.zig

643
src/analytics/analytics.zig Normal file
View File

@@ -0,0 +1,643 @@
const std = @import("std");
pub const Reader = struct {
const Self = @This();
pub const ReadError = error{EOF};
buf: []u8,
remain: []u8,
allocator: *std.mem.Allocator,
pub fn init(buf: []u8, allocator: *std.mem.Allocator) Reader {
return Reader{
.buf = buf,
.remain = buf,
.allocator = allocator,
};
}
pub fn read(this: *Self, count: usize) ![]u8 {
const read_count = std.math.min(count, this.remain.len);
if (read_count < count) {
return error.EOF;
}
var slice = this.remain[0..read_count];
this.remain = this.remain[read_count..];
return slice;
}
pub fn readAs(this: *Self, comptime T: type) !T {
if (!std.meta.trait.hasUniqueRepresentation(T)) {
@compileError(@typeName(T) ++ " must have unique representation.");
}
return std.mem.bytesAsValue(T, try this.read(@sizeOf(T)));
}
pub fn readByte(this: *Self) !u8 {
return (try this.read(1))[0];
}
pub fn readEnum(this: *Self, comptime Enum: type) !Enum {
const E = error{
/// An integer was read, but it did not match any of the tags in the supplied enum.
InvalidValue,
};
const type_info = @typeInfo(Enum).Enum;
const tag = try this.readInt(type_info.tag_type);
inline for (std.meta.fields(Enum)) |field| {
if (tag == field.value) {
return @field(Enum, field.name);
}
}
return E.InvalidValue;
}
pub fn readArray(this: *Self, comptime T: type) ![]const T {
const length = try this.readInt(u32);
if (length == 0) {
return &([_]T{});
}
switch (T) {
u8 => {
return try this.read(length);
},
u16, u32, i8, i16, i32 => {
return std.mem.readIntSliceNative(T, this.read(length * @sizeOf(T)));
},
[]const u8 => {
var i: u32 = 0;
var array = try this.allocator.alloc([]const u8, length);
while (i < length) : (i += 1) {
array[i] = try this.readArray(u8);
}
return array;
},
else => {
switch (@typeInfo(T)) {
.Struct => |Struct| {
switch (Struct.layout) {
.Packed => {
const sizeof = @sizeOf(T);
var slice = try this.read(sizeof * length);
return std.mem.bytesAsSlice(T, slice);
},
else => {},
}
},
.Enum => |type_info| {
const enum_values = try this.read(length * @sizeOf(type_info.tag_type));
return @ptrCast([*]T, enum_values.ptr)[0..length];
},
else => {},
}
var i: u32 = 0;
var array = try this.allocator.alloc(T, length);
while (i < length) : (i += 1) {
array[i] = try this.readValue(T);
}
return array;
},
}
}
pub fn readByteArray(this: *Self) ![]u8 {
const length = try this.readInt(u32);
if (length == 0) {
return &([_]u8{});
}
return try this.read(@intCast(usize, length));
}
pub fn readInt(this: *Self, comptime T: type) !T {
var slice = try this.read(@sizeOf(T));
return std.mem.readIntSliceNative(T, slice);
}
pub fn readBool(this: *Self) !bool {
return (try this.readByte()) > 0;
}
pub fn readValue(this: *Self, comptime T: type) !T {
switch (T) {
bool => {
return try this.readBool();
},
u8 => {
return try this.readByte();
},
[]const u8 => {
return try this.readArray(u8);
},
[]const []const u8 => {
return try this.readArray([]const u8);
},
[]u8 => {
return try this.readArray([]u8);
},
u16, u32, i8, i16, i32 => {
return std.mem.readIntSliceNative(T, try this.read(@sizeOf(T)));
},
else => {
switch (@typeInfo(T)) {
.Struct => |Struct| {
switch (Struct.layout) {
.Packed => {
const sizeof = @sizeOf(T);
var slice = try this.read(sizeof);
return @ptrCast(*T, slice[0..sizeof]).*;
},
else => {},
}
},
.Enum => |type_info| {
return try this.readEnum(T);
},
else => {},
}
return try T.decode(this);
},
}
@compileError("Invalid type passed to readValue");
}
};
pub fn Writer(comptime WritableStream: type) type {
return struct {
const Self = @This();
writable: WritableStream,
pub fn init(writable: WritableStream) Self {
return Self{ .writable = writable };
}
pub fn write(this: *Self, bytes: anytype) !void {
_ = try this.writable.write(bytes);
}
pub fn writeByte(this: *Self, byte: u8) !void {
_ = try this.writable.write(&[1]u8{byte});
}
pub fn writeInt(this: *Self, int: anytype) !void {
try this.write(std.mem.asBytes(&int));
}
pub fn writeFieldID(this: *Self, comptime id: comptime_int) !void {
try this.writeByte(id);
}
pub fn writeEnum(this: *Self, val: anytype) !void {
try this.writeInt(@enumToInt(val));
}
pub fn writeValue(this: *Self, slice: anytype) !void {
switch (@TypeOf(slice)) {
[]u16,
[]u32,
[]i16,
[]i32,
[]i8,
[]const u16,
[]const u32,
[]const i16,
[]const i32,
[]const i8,
=> {
try this.writeArray(@TypeOf(slice), slice);
},
[]u8, []const u8 => {
try this.writeArray(u8, slice);
},
u8 => {
try this.write(slice);
},
u16, u32, i16, i32, i8 => {
try this.write(std.mem.asBytes(slice));
},
else => {
try slice.encode(this);
},
}
}
pub fn writeArray(this: *Self, comptime T: type, slice: anytype) !void {
try this.writeInt(@truncate(u32, slice.len));
switch (T) {
u8 => {
try this.write(slice);
},
u16, u32, i16, i32, i8 => {
try this.write(std.mem.asBytes(slice));
},
[]u8,
[]u16,
[]u32,
[]i16,
[]i32,
[]i8,
[]const u8,
[]const u16,
[]const u32,
[]const i16,
[]const i32,
[]const i8,
=> {
for (slice) |num_slice| {
try this.writeArray(std.meta.Child(@TypeOf(num_slice)), num_slice);
}
},
else => {
for (slice) |val| {
try val.encode(this);
}
},
}
}
pub fn endMessage(this: *Self) !void {
try this.writeByte(0);
}
};
}
pub const ByteWriter = Writer(*std.io.FixedBufferStream([]u8));
pub const FileWriter = Writer(std.fs.File);
pub const Analytics = struct {
pub const OperatingSystem = enum(u8) {
_none,
/// linux
linux,
/// macos
macos,
/// windows
windows,
/// wsl
wsl,
_,
pub fn jsonStringify(self: *const @This(), opts: anytype, o: anytype) !void {
return try std.json.stringify(@tagName(self), opts, o);
}
};
pub const Architecture = enum(u8) {
_none,
/// x64
x64,
/// arm
arm,
_,
pub fn jsonStringify(self: *const @This(), opts: anytype, o: anytype) !void {
return try std.json.stringify(@tagName(self), opts, o);
}
};
pub const Platform = struct {
/// os
os: OperatingSystem,
/// arch
arch: Architecture,
/// version
version: []const u8,
pub fn decode(reader: anytype) anyerror!Platform {
var this = std.mem.zeroes(Platform);
this.os = try reader.readValue(OperatingSystem);
this.arch = try reader.readValue(Architecture);
this.version = try reader.readValue([]const u8);
return this;
}
pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
try writer.writeEnum(this.os);
try writer.writeEnum(this.arch);
try writer.writeValue(this.version);
}
};
pub const EventKind = enum(u32) {
_none,
/// bundle_success
bundle_success,
/// bundle_fail
bundle_fail,
/// http_start
http_start,
/// http_build
http_build,
/// bundle_start
bundle_start,
_,
pub fn jsonStringify(self: *const @This(), opts: anytype, o: anytype) !void {
return try std.json.stringify(@tagName(self), opts, o);
}
};
pub const Uint64 = packed struct {
/// first
first: u32 = 0,
/// second
second: u32 = 0,
pub fn decode(reader: anytype) anyerror!Uint64 {
var this = std.mem.zeroes(Uint64);
this.first = try reader.readValue(u32);
this.second = try reader.readValue(u32);
return this;
}
pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
try writer.writeInt(this.first);
try writer.writeInt(this.second);
}
};
pub const EventListHeader = struct {
/// machine_id
machine_id: Uint64,
/// platform
platform: Platform,
/// build_id
build_id: u32 = 0,
/// session_length
session_length: u32 = 0,
pub fn decode(reader: anytype) anyerror!EventListHeader {
var this = std.mem.zeroes(EventListHeader);
this.machine_id = try reader.readValue(Uint64);
this.platform = try reader.readValue(Platform);
this.build_id = try reader.readValue(u32);
this.session_length = try reader.readValue(u32);
return this;
}
pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
try writer.writeValue(this.machine_id);
try writer.writeValue(this.platform);
try writer.writeInt(this.build_id);
try writer.writeInt(this.session_length);
}
};
pub const EventHeader = struct {
/// timestamp
timestamp: Uint64,
/// kind
kind: EventKind,
pub fn decode(reader: anytype) anyerror!EventHeader {
var this = std.mem.zeroes(EventHeader);
this.timestamp = try reader.readValue(Uint64);
this.kind = try reader.readValue(EventKind);
return this;
}
pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
try writer.writeValue(this.timestamp);
try writer.writeEnum(this.kind);
}
};
pub const EventList = struct {
/// header
header: EventListHeader,
/// event_count
event_count: u32 = 0,
pub fn decode(reader: anytype) anyerror!EventList {
var this = std.mem.zeroes(EventList);
this.header = try reader.readValue(EventListHeader);
this.event_count = try reader.readValue(u32);
return this;
}
pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
try writer.writeValue(this.header);
try writer.writeInt(this.event_count);
}
};
};
const ExamplePackedStruct = packed struct {
len: u32 = 0,
offset: u32 = 0,
pub fn encode(this: *const ExamplePackedStruct, writer: anytype) !void {
try writer.write(std.mem.asBytes(this));
}
pub fn decode(reader: anytype) !ExamplePackedStruct {
return try reader.readAs(ExamplePackedStruct);
}
};
const ExampleStruct = struct {
name: []const u8 = "",
age: u32 = 0,
pub fn encode(this: *const ExampleStruct, writer: anytype) !void {
try writer.writeArray(u8, this.name);
try writer.writeInt(this.age);
}
pub fn decode(reader: anytype) !ExampleStruct {
var this = std.mem.zeroes(ExampleStruct);
this.name = try reader.readArray(u8);
this.age = try reader.readInt(u32);
return this;
}
};
const EnumValue = enum(u8) { hey, hi, heyopoo };
const ExampleMessage = struct {
examples: ?[]ExampleStruct = &([_]ExampleStruct{}),
pack: ?[]ExamplePackedStruct = &([_]ExamplePackedStruct{}),
hey: ?u8 = 0,
hey16: ?u16 = 0,
hey32: ?u16 = 0,
heyi32: ?i32 = 0,
heyi16: ?i16 = 0,
heyi8: ?i8 = 0,
boolean: ?bool = null,
heyooo: ?EnumValue = null,
pub fn encode(this: *const ExampleMessage, writer: anytype) !void {
if (this.examples) |examples| {
try writer.writeFieldID(1);
try writer.writeArray(ExampleStruct, examples);
}
if (this.pack) |pack| {
try writer.writeFieldID(2);
try writer.writeArray(ExamplePackedStruct, pack);
}
if (this.hey) |hey| {
try writer.writeFieldID(3);
try writer.writeInt(hey);
}
if (this.hey16) |hey16| {
try writer.writeFieldID(4);
try writer.writeInt(hey16);
}
if (this.hey32) |hey32| {
try writer.writeFieldID(5);
try writer.writeInt(hey32);
}
if (this.heyi32) |heyi32| {
try writer.writeFieldID(6);
try writer.writeInt(heyi32);
}
if (this.heyi16) |heyi16| {
try writer.writeFieldID(7);
try writer.writeInt(heyi16);
}
if (this.heyi8) |heyi8| {
try writer.writeFieldID(8);
try writer.writeInt(heyi8);
}
if (this.boolean) |boolean| {
try writer.writeFieldID(9);
try writer.writeInt(boolean);
}
if (this.heyooo) |heyoo| {
try writer.writeFieldID(10);
try writer.writeEnum(heyoo);
}
try writer.endMessage();
}
pub fn decode(reader: anytype) !ExampleMessage {
var this = std.mem.zeroes(ExampleMessage);
while (true) {
switch (try reader.readByte()) {
0 => {
return this;
},
1 => {
this.examples = try reader.readArray(std.meta.Child(@TypeOf(this.examples.?)));
},
2 => {
this.pack = try reader.readArray(std.meta.Child(@TypeOf(this.pack.?)));
},
3 => {
this.hey = try reader.readValue(@TypeOf(this.hey.?));
},
4 => {
this.hey16 = try reader.readValue(@TypeOf(this.hey16.?));
},
5 => {
this.hey32 = try reader.readValue(@TypeOf(this.hey32.?));
},
6 => {
this.heyi32 = try reader.readValue(@TypeOf(this.heyi32.?));
},
7 => {
this.heyi16 = try reader.readValue(@TypeOf(this.heyi16.?));
},
8 => {
this.heyi8 = try reader.readValue(@TypeOf(this.heyi8.?));
},
9 => {
this.boolean = try reader.readValue(@TypeOf(this.boolean.?));
},
10 => {
this.heyooo = try reader.readValue(@TypeOf(this.heyooo.?));
},
else => {
return error.InvalidValue;
},
}
}
return this;
}
};
test "ExampleMessage" {
var base = std.mem.zeroes(ExampleMessage);
base.hey = 1;
var buf: [4096]u8 = undefined;
var writable = std.io.fixedBufferStream(&buf);
var writer = ByteWriter.init(writable);
var examples = [_]ExamplePackedStruct{
.{ .len = 2, .offset = 5 },
.{ .len = 0, .offset = 10 },
};
var more_examples = [_]ExampleStruct{
.{ .name = "bacon", .age = 10 },
.{ .name = "slime", .age = 300 },
};
base.examples = &more_examples;
base.pack = &examples;
base.heyooo = EnumValue.hey;
try base.encode(&writer);
var reader = Reader.init(&buf, std.heap.c_allocator);
var compare = try ExampleMessage.decode(&reader);
try std.testing.expectEqual(base.hey orelse 255, 1);
const cmp_pack = compare.pack.?;
for (cmp_pack) |item, id| {
try std.testing.expectEqual(item, examples[id]);
}
const cmp_ex = compare.examples.?;
for (cmp_ex) |item, id| {
try std.testing.expectEqualStrings(item.name, more_examples[id].name);
try std.testing.expectEqual(item.age, more_examples[id].age);
}
try std.testing.expectEqual(cmp_pack[0].len, examples[0].len);
try std.testing.expectEqual(base.heyooo, compare.heyooo);
}

View File

@@ -0,0 +1,341 @@
usingnamespace @import("../global.zig");
const sync = @import("../sync.zig");
const std = @import("std");
const HTTPClient = @import("../http_client.zig");
const URL = @import("../query_string_map.zig").URL;
const Fs = @import("../fs.zig");
const Analytics = @import("./analytics.zig").Analytics;
const Writer = @import("./analytics.zig").Writer;
const Headers = @import("../javascript/jsc/webcore/response.zig").Headers;
pub const EventName = enum(u8) {
bundle_success,
bundle_fail,
bundle_start,
http_start,
http_build,
};
const platform_arch = if (Environment.isAarch64) Analytics.Architecture.arm else Analytics.Architecture.x64;
pub const Event = struct {
timestamp: u64,
data: Data,
pub fn init(comptime name: EventName) Event {
const millis = std.time.milliTimestamp();
const timestamp = if (millis < 0) 0 else @intCast(u64, millis);
return Event{ .timestamp = timestamp, .data = @unionInit(Data, @tagName(name), void{}) };
}
};
pub const Data = union(EventName) {
bundle_success: void,
bundle_fail: void,
bundle_start: void,
http_start: void,
http_build: void,
pub fn toKind(this: Data) Analytics.EventKind {
return switch (this) {
.bundle_success => .bundle_success,
.bundle_fail => .bundle_fail,
.bundle_start => .bundle_start,
.http_start => .http_start,
.http_build => .http_build,
};
}
};
const EventQueue = sync.Channel(Event, .Dynamic);
var event_queue: EventQueue = undefined;
pub const GenerateHeader = struct {
pub fn generate() Analytics.EventListHeader {
if (Environment.isMac) {
return Analytics.EventListHeader{
.machine_id = GenerateMachineID.forMac() catch Analytics.Uint64{},
.platform = GeneratePlatform.forMac(),
.build_id = comptime @truncate(u32, Global.build_id),
};
}
if (Environment.isLinux) {
return Analytics.EventListHeader{
.machine_id = GenerateMachineID.forLinux() catch Analytics.Uint64{},
.platform = GeneratePlatform.forLinux(),
.build_id = comptime @truncate(u32, Global.build_id),
};
}
unreachable;
}
pub const GeneratePlatform = struct {
var osversion_name: [32]u8 = undefined;
pub fn forMac() Analytics.Platform {
std.mem.set(u8, std.mem.span(&osversion_name), 0);
var platform = Analytics.Platform{ .os = Analytics.OperatingSystem.macos, .version = "", .arch = platform_arch };
var osversion_name_buf: [2]c_int = undefined;
var osversion_name_ptr = osversion_name.len - 1;
var len = osversion_name.len - 1;
if (std.c.sysctlbyname("kern.osrelease", &osversion_name, &len, null, 0) == -1) return platform;
platform.version = std.mem.span(std.mem.sliceTo(std.mem.span(&osversion_name), @as(u8, 0)));
return platform;
}
pub var linux_os_name: std.c.utsname = undefined;
pub fn forLinux() Analytics.Platform {
linux_os_name = std.mem.zeroes(linux_os_name);
std.c.uname(&linux_os_name);
const release = std.mem.span(linux_os_name.release);
const version = std.mem.sliceTo(std.mem.span(linux_os_name.version).ptr, @as(u8, 0));
// Linux DESKTOP-P4LCIEM 5.10.16.3-microsoft-standard-WSL2 #1 SMP Fri Apr 2 22:23:49 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
if (std.mem.indexOf(u8, release, "microsoft") != null) {
return Analytics.Platform{ .os = Analytics.OperatingSystem.wsl, .version = version, .arch = platform_arch };
}
return Analytics.Platform{ .os = Analytics.OperatingSystem.linux, .version = version, .arch = platform_arch };
}
};
// https://github.com/denisbrodbeck/machineid
pub const GenerateMachineID = struct {
pub fn forMac() !Analytics.Uint64 {
const cmds = [_]string{
"/usr/sbin/ioreg",
"-rd1",
"-c",
"IOPlatformExpertDevice",
};
const result = try std.ChildProcess.exec(.{
.allocator = default_allocator,
.cwd = Fs.FileSystem.instance.top_level_dir,
.argv = std.mem.span(&cmds),
});
var out: []const u8 = result.stdout;
var offset: usize = 0;
offset = std.mem.lastIndexOf(u8, result.stdout, "\"IOPlatformUUID\"") orelse return Analytics.Uint64{};
out = std.mem.trimLeft(u8, out[offset + "\"IOPlatformUUID\"".len ..], " \n\t=");
if (out.len == 0 or out[0] != '"') return Analytics.Uint64{};
out = out[1..];
offset = std.mem.indexOfScalar(u8, out, '"') orelse return Analytics.Uint64{};
out = out[0..offset];
const hash = std.hash.Wyhash.hash(0, std.mem.trim(u8, out, "\n\r "));
var hash_bytes = std.mem.asBytes(&hash);
return Analytics.Uint64{
.first = std.mem.readIntNative(u32, hash_bytes[0..4]),
.second = std.mem.readIntNative(u32, hash_bytes[4..8]),
};
}
pub var linux_machine_id: [256]u8 = undefined;
pub fn forLinux() !Analytics.Uint64 {
var file = std.fs.openFileAbsoluteZ("/var/lib/dbus/machine-id", .{ .read = true }) catch |err| brk: {
break :brk try std.fs.openFileAbsoluteZ("/etc/machine-id", .{ .read = true });
};
defer file.close();
var read_count = try file.read(&linux_machine_id);
const hash = std.hash.Wyhash.hash(0, std.mem.trim(u8, linux_machine_id[0..read_count], "\n\r "));
var hash_bytes = std.mem.asBytes(&hash);
return Analytics.Uint64{
.first = std.mem.readIntNative(u32, hash_bytes[0..4]),
.second = std.mem.readIntNative(u32, hash_bytes[4..8]),
};
}
};
};
pub var has_loaded = false;
pub var disabled = false;
pub fn enqueue(comptime name: EventName) void {
if (disabled) return;
if (!has_loaded) {
defer has_loaded = true;
event_queue = EventQueue.init(std.heap.c_allocator);
spawn() catch |err| {
if (comptime isDebug) {
Output.prettyErrorln("[Analytics] error spawning thread {s}", .{@errorName(err)});
Output.flush();
}
disabled = true;
return;
};
}
_ = event_queue.tryWriteItem(Event.init(name)) catch false;
}
pub var thread: std.Thread = undefined;
pub fn spawn() !void {
@setCold(true);
has_loaded = true;
thread = try std.Thread.spawn(.{}, readloop, .{});
}
fn readloop() anyerror!void {
defer disabled = true;
Output.Source.configureThread();
defer Output.flush();
thread.setName("Analytics") catch {};
var event_list = EventList.init();
// everybody's random should be random
while (true) {
while (event_queue.tryReadItem() catch null) |item| {
event_list.push(item);
}
if (event_list.events.items.len > 0) {
event_list.flush();
}
event_queue.getters.wait(&event_queue.mutex);
}
}
pub const EventList = struct {
header: Analytics.EventListHeader,
events: std.ArrayList(Event),
client: HTTPClient,
out_buffer: MutableString,
in_buffer: std.ArrayList(u8),
var random: std.rand.DefaultPrng = undefined;
pub fn init() EventList {
random = std.rand.DefaultPrng.init(@intCast(u64, std.time.milliTimestamp()));
return EventList{
.header = GenerateHeader.generate(),
.events = std.ArrayList(Event).init(default_allocator),
.in_buffer = std.ArrayList(u8).init(default_allocator),
.client = HTTPClient.init(
default_allocator,
.POST,
URL.parse(Environment.analytics_url),
Headers.Entries{},
"",
),
.out_buffer = MutableString.init(default_allocator, 0) catch unreachable,
};
}
pub fn push(this: *EventList, event: Event) void {
this.events.append(event) catch unreachable;
}
pub fn flush(this: *EventList) void {
this._flush() catch |err| {
Output.prettyErrorln("[Analytics] Error: {s}", .{@errorName(err)});
Output.flush();
};
}
pub var is_stuck = false;
fn _flush(this: *EventList) !void {
this.in_buffer.clearRetainingCapacity();
const AnalyticsWriter = Writer(*std.ArrayList(u8).Writer);
var in_buffer = &this.in_buffer;
var buffer_writer = in_buffer.writer();
var analytics_writer = AnalyticsWriter.init(&buffer_writer);
const start_time = @import("root").start_time;
const now = std.time.nanoTimestamp();
this.header.session_length = @truncate(u32, @intCast(u64, (now - start_time)) / std.time.ns_per_ms);
var list = Analytics.EventList{
.header = this.header,
.event_count = @intCast(u32, this.events.items.len),
};
try list.encode(&analytics_writer);
for (this.events.items) |_event| {
const event: Event = _event;
var time_bytes = std.mem.asBytes(&event.timestamp);
const analytics_event = Analytics.EventHeader{
.timestamp = Analytics.Uint64{
.first = std.mem.readIntNative(u32, time_bytes[0..4]),
.second = std.mem.readIntNative(u32, time_bytes[4..8]),
},
.kind = event.data.toKind(),
};
try analytics_event.encode(&analytics_writer);
}
const count = this.events.items.len;
if (comptime FeatureFlags.verbose_analytics) {
Output.prettyErrorln("[Analytics] Sending {d} events", .{count});
Output.flush();
}
this.events.clearRetainingCapacity();
var retry_remaining: usize = 10;
retry: while (retry_remaining > 0) {
const response = this.client.send(this.in_buffer.items, &this.out_buffer) catch |err| {
if (FeatureFlags.verbose_analytics) {
Output.prettyErrorln("[Analytics] failed due to error {s} ({d} retries remain)", .{ @errorName(err), retry_remaining });
}
retry_remaining -= 1;
@atomicStore(bool, &is_stuck, true, .Release);
const min_delay = (11 - retry_remaining) * std.time.ns_per_s / 2;
Output.flush();
std.time.sleep(random.random.intRangeAtMost(u64, min_delay, min_delay * 2));
continue :retry;
};
if (response.status_code >= 500 and response.status_code <= 599) {
if (FeatureFlags.verbose_analytics) {
Output.prettyErrorln("[Analytics] failed due to status code {d} ({d} retries remain)", .{ response.status_code, retry_remaining });
}
retry_remaining -= 1;
@atomicStore(bool, &is_stuck, true, .Release);
const min_delay = (11 - retry_remaining) * std.time.ns_per_s / 2;
Output.flush();
std.time.sleep(random.random.intRangeAtMost(u64, min_delay, min_delay * 2));
continue :retry;
}
break :retry;
}
@atomicStore(bool, &is_stuck, retry_remaining == 0, .Release);
this.in_buffer.clearRetainingCapacity();
this.out_buffer.reset();
if (comptime FeatureFlags.verbose_analytics) {
Output.prettyErrorln("[Analytics] Sent {d} events", .{count});
Output.flush();
}
}
};
pub var is_ci = false;

View File

@@ -0,0 +1,49 @@
package Analytics;
smol OperatingSystem {
linux = 1;
macos = 2;
windows = 3;
wsl = 4;
}
smol Architecture {
x64 = 1;
arm = 2;
}
struct Platform {
OperatingSystem os;
Architecture arch;
string version;
}
enum EventKind {
bundle_success = 1;
bundle_fail = 2;
http_start = 3;
http_build = 4;
bundle_start = 5;
}
struct Uint64 {
uint32 first;
uint32 second;
}
struct EventListHeader {
Uint64 machine_id;
Platform platform;
uint32 build_id;
uint32 session_length;
}
struct EventHeader {
Uint64 timestamp;
EventKind kind;
}
struct EventList {
EventListHeader header;
uint32 event_count;
}

View File

@@ -38,6 +38,7 @@ const Lock = @import("./lock.zig").Lock;
const NewBunQueue = @import("./bun_queue.zig").NewBunQueue;
const NodeFallbackModules = @import("./node_fallbacks.zig");
const CacheEntry = @import("./cache.zig").FsCacheEntry;
const Analytics = @import("./analytics/analytics_thread.zig");
const Linker = linker.Linker;
const Resolver = _resolver.Resolver;
@@ -231,8 +232,23 @@ pub const Bundler = struct {
try this.env.load(&this.fs.fs, dir, true);
}
},
.disable => {
this.env.loadProcess();
},
else => {},
}
if (this.env.map.get("DISABLE_BUN_ANALYTICS")) |should_disable| {
if (strings.eqlComptime(should_disable, "1")) {
Analytics.disabled = true;
}
}
if (this.env.map.get("CI")) |IS_CI| {
if (strings.eqlComptime(IS_CI, "true")) {
Analytics.is_ci = true;
}
}
}
// This must be run after a framework is configured, if a framework is enabled

View File

@@ -22,3 +22,5 @@ pub const isRelease = std.builtin.Mode.Debug != std.builtin.mode and !isTest;
pub const isTest = std.builtin.is_test;
pub const isLinux = std.Target.current.os.tag == .linux;
pub const isAarch64 = std.Target.current.cpu.arch == .aarch64;
pub const analytics_url = "http://localhost:3008/events";

View File

@@ -72,3 +72,5 @@ pub const is_macro_enabled = true;
pub const force_macro = false;
pub const include_filename_in_jsx = false;
pub const verbose_analytics = true;

View File

@@ -21,6 +21,7 @@ const OutputFile = Options.OutputFile;
const DotEnv = @import("./env_loader.zig");
const mimalloc = @import("./allocators/mimalloc.zig");
const MacroMap = @import("./resolver/package_json.zig").MacroMap;
const Analytics = @import("./analytics/analytics_thread.zig");
pub fn constStrToU8(s: string) []u8 {
return @intToPtr([*]u8, @ptrToInt(s.ptr))[0..s.len];
}
@@ -544,6 +545,14 @@ pub const RequestContext = struct {
return ctx;
}
pub inline fn isBrowserNavigation(req: *RequestContext) bool {
if (req.header("Sec-Fetch-Mode")) |mode| {
return strings.eqlComptime(mode.value, "navigate");
}
return false;
}
pub fn sendNotFound(req: *RequestContext) !void {
std.debug.assert(!req.has_called_done);
@@ -2686,6 +2695,7 @@ pub const Server = struct {
server.detectTSConfig();
try server.initWatcher();
did_init = true;
Analytics.enqueue(Analytics.EventName.http_start);
server.handleConnection(&conn, comptime features);
}
@@ -2751,6 +2761,9 @@ pub const Server = struct {
var req_ctx = &req_ctx_;
req_ctx.timer.reset();
const is_navigation_request = req_ctx_.isBrowserNavigation();
defer if (is_navigation_request) Analytics.enqueue(Analytics.EventName.http_build);
if (req_ctx.url.needs_redirect) {
req_ctx.handleRedirect(req_ctx.url.path) catch |err| {
Output.prettyErrorln("<r>[<red>{s}<r>] - <b>{s}<r>: {s}", .{ @errorName(err), req.method, req.path });