sentiero 1.0.0.beta1 → 1.0.0.beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/sentiero/analytics/error_discovery.rb +7 -0
- data/lib/sentiero/stores/sqlite/payload_codec.rb +42 -0
- data/lib/sentiero/stores/sqlite/schema.rb +29 -0
- data/lib/sentiero/stores/sqlite.rb +4 -3
- data/lib/sentiero/version.rb +1 -1
- data/lib/sentiero/web/templates/client_error_show.html.erb +6 -1
- data/lib/sentiero/web/views/client_error_show_view.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed5c945cbabf6b9ae1ab2b3e10474d06bc06bda78c1a4b381b69845791c18296
|
|
4
|
+
data.tar.gz: abb85bfebbdb1e8c48cba2eb844b6446d5bd82f17e7727c3b00347b5256de38e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5ef1855f03fc35d2b719ee316979de2ae3331b2400bfb534a0406048fe31cd9b292e252fdda5a59f54f73a066c33aed17aecaf88c2e697a82d0269ddd525f84
|
|
7
|
+
data.tar.gz: c7fce88e343ca579a5add1656f6e12b997162a5c7dbe62630e21b00334e13e3bc88f51dcabc9878f8ad93712851b626f5475cd76bec870eb1f142a7089959021
|
|
@@ -48,6 +48,7 @@ module Sentiero
|
|
|
48
48
|
|
|
49
49
|
key = ErrorCollector.group_key(message)
|
|
50
50
|
group = groups[key] ||= new_group(key, message, payload)
|
|
51
|
+
group[:stack] ||= stack_of(payload)
|
|
51
52
|
group[:count] += 1
|
|
52
53
|
group[:last_seen_at] = [group[:last_seen_at], timestamp].compact.max
|
|
53
54
|
tally_facets(group, summary[:metadata] || {})
|
|
@@ -72,6 +73,7 @@ module Sentiero
|
|
|
72
73
|
message: message,
|
|
73
74
|
source: source_of(payload),
|
|
74
75
|
line: line_of(payload),
|
|
76
|
+
stack: nil,
|
|
75
77
|
count: 0,
|
|
76
78
|
last_seen_at: nil,
|
|
77
79
|
browsers: Hash.new(0),
|
|
@@ -105,6 +107,11 @@ module Sentiero
|
|
|
105
107
|
line.is_a?(Integer) ? line : nil
|
|
106
108
|
end
|
|
107
109
|
|
|
110
|
+
def stack_of(payload)
|
|
111
|
+
stack = payload["stack"]
|
|
112
|
+
(stack.is_a?(String) && !stack.empty?) ? stack : nil
|
|
113
|
+
end
|
|
114
|
+
|
|
108
115
|
def sort_groups(groups, sort_by)
|
|
109
116
|
case sort_by
|
|
110
117
|
when "recency"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Sentiero
|
|
6
|
+
module Stores
|
|
7
|
+
class SQLite
|
|
8
|
+
# zlib compression for events.data payloads. FullSnapshots dominate the
|
|
9
|
+
# table's bytes (~93% in production samples) and deflate ~6-7x, so
|
|
10
|
+
# payloads at or above the threshold are stored as compressed BLOBs;
|
|
11
|
+
# smaller rows stay plain JSON TEXT (near-zero win, and skipping them
|
|
12
|
+
# keeps ~85% of rows out of the deflate path). Reads sniff the storage
|
|
13
|
+
# format from the payload itself: a zlib stream starts with 0x78 and a
|
|
14
|
+
# valid header checksum, which no JSON text can.
|
|
15
|
+
module PayloadCodec
|
|
16
|
+
COMPRESSION_THRESHOLD = 1024
|
|
17
|
+
# CMF byte for deflate with a 32K window — the only one zlib emits.
|
|
18
|
+
ZLIB_FIRST_BYTE = 0x78
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def encode(json)
|
|
23
|
+
return json if json.bytesize < COMPRESSION_THRESHOLD
|
|
24
|
+
|
|
25
|
+
::SQLite3::Blob.new(Zlib::Deflate.deflate(json))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def decode(data)
|
|
29
|
+
return data unless compressed?(data)
|
|
30
|
+
|
|
31
|
+
Zlib::Inflate.inflate(data).force_encoding(Encoding::UTF_8)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def compressed?(data)
|
|
35
|
+
data.getbyte(0) == ZLIB_FIRST_BYTE &&
|
|
36
|
+
(flg = data.getbyte(1)) &&
|
|
37
|
+
(((ZLIB_FIRST_BYTE << 8) | flg) % 31).zero?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -83,6 +83,7 @@ module Sentiero
|
|
|
83
83
|
SQL
|
|
84
84
|
|
|
85
85
|
migrate_events_type!(db)
|
|
86
|
+
migrate_compress_payloads!(db)
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
# Databases created before the events.type column existed get a
|
|
@@ -110,6 +111,34 @@ module Sentiero
|
|
|
110
111
|
SQL
|
|
111
112
|
end
|
|
112
113
|
end
|
|
114
|
+
|
|
115
|
+
# user_version 1 = every large payload is zlib-compressed (PayloadCodec).
|
|
116
|
+
SCHEMA_VERSION = 1
|
|
117
|
+
|
|
118
|
+
# One-time pass compressing pre-existing plain-TEXT payloads. Batched
|
|
119
|
+
# in short transactions so a live recorder's ingest writes interleave
|
|
120
|
+
# instead of timing out behind one giant lock. Guarded by
|
|
121
|
+
# PRAGMA user_version, so subsequent opens skip the table scan.
|
|
122
|
+
# Frees pages for reuse but does not shrink the file — VACUUM manually
|
|
123
|
+
# to reclaim disk.
|
|
124
|
+
def self.migrate_compress_payloads!(db)
|
|
125
|
+
return if db.get_first_value("PRAGMA user_version").to_i >= SCHEMA_VERSION
|
|
126
|
+
|
|
127
|
+
loop do
|
|
128
|
+
rows = db.execute(
|
|
129
|
+
"SELECT id, data FROM events WHERE typeof(data) = 'text' AND LENGTH(data) >= ? LIMIT 500",
|
|
130
|
+
[PayloadCodec::COMPRESSION_THRESHOLD]
|
|
131
|
+
)
|
|
132
|
+
break if rows.empty?
|
|
133
|
+
|
|
134
|
+
db.transaction(:immediate) do
|
|
135
|
+
rows.each do |row|
|
|
136
|
+
db.execute("UPDATE events SET data = ? WHERE id = ?", [PayloadCodec.encode(row["data"]), row["id"]])
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
db.execute("PRAGMA user_version = #{SCHEMA_VERSION}")
|
|
141
|
+
end
|
|
113
142
|
end
|
|
114
143
|
end
|
|
115
144
|
end
|
|
@@ -16,6 +16,7 @@ module Sentiero
|
|
|
16
16
|
class SQLite < Store
|
|
17
17
|
# Loaded after the class line above establishes SQLite < Store, so
|
|
18
18
|
# schema.rb's own `class SQLite` reopen doesn't hit a superclass mismatch.
|
|
19
|
+
require_relative "sqlite/payload_codec"
|
|
19
20
|
require_relative "sqlite/schema"
|
|
20
21
|
|
|
21
22
|
# Single-file SQLite store for single-process production and dev.
|
|
@@ -101,7 +102,7 @@ module Sentiero
|
|
|
101
102
|
events.each do |event|
|
|
102
103
|
type = event["type"]
|
|
103
104
|
stmt.execute(session_id, window_id, event["timestamp"]&.to_f,
|
|
104
|
-
type.is_a?(Integer) ? type : nil, JSON.generate(event))
|
|
105
|
+
type.is_a?(Integer) ? type : nil, PayloadCodec.encode(JSON.generate(event)))
|
|
105
106
|
end
|
|
106
107
|
ensure
|
|
107
108
|
stmt.close
|
|
@@ -300,7 +301,7 @@ module Sentiero
|
|
|
300
301
|
params << limit.to_i
|
|
301
302
|
end
|
|
302
303
|
|
|
303
|
-
db.execute(sql, params).map { |event_row| JSON.parse(event_row["data"]) }
|
|
304
|
+
db.execute(sql, params).map { |event_row| JSON.parse(PayloadCodec.decode(event_row["data"])) }
|
|
304
305
|
end
|
|
305
306
|
|
|
306
307
|
def save_metadata(session_id, metadata)
|
|
@@ -570,7 +571,7 @@ module Sentiero
|
|
|
570
571
|
"SELECT session_id, window_id, data FROM events WHERE session_id IN (#{placeholders})#{type_clause} ORDER BY timestamp ASC",
|
|
571
572
|
chunk + type_params
|
|
572
573
|
).each do |row|
|
|
573
|
-
(grouped[row["session_id"]][row["window_id"]] ||= []) << JSON.parse(row["data"])
|
|
574
|
+
(grouped[row["session_id"]][row["window_id"]] ||= []) << JSON.parse(PayloadCodec.decode(row["data"]))
|
|
574
575
|
end
|
|
575
576
|
end
|
|
576
577
|
grouped
|
data/lib/sentiero/version.rb
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
<div class="text-xs text-gray-600">
|
|
24
24
|
<%= view.group[:last_seen_at] ? Time.at(view.group[:last_seen_at].to_f / 1000).utc.strftime("%b %d, %Y %H:%M:%S UTC") : "N/A" %>
|
|
25
25
|
</div>
|
|
26
|
+
|
|
27
|
+
<% if view.group[:stack] -%>
|
|
28
|
+
<div class="text-[10px] font-medium text-gray-400 uppercase tracking-wider pt-0.5">Stack</div>
|
|
29
|
+
<pre class="text-[10px] text-gray-500 bg-gray-50 rounded p-2 overflow-x-auto whitespace-pre-wrap font-mono" data-client-error-stack><%= view.h(view.group[:stack]) %></pre>
|
|
30
|
+
<% end -%>
|
|
26
31
|
</div>
|
|
27
32
|
</div>
|
|
28
33
|
</div>
|
|
@@ -36,7 +41,7 @@
|
|
|
36
41
|
<% view.facet_chips.each do |title, counts| -%>
|
|
37
42
|
<% next if counts.empty? -%>
|
|
38
43
|
<div class="flex items-center gap-2 mb-1.5 flex-wrap">
|
|
39
|
-
<span class="text-[10px] font-medium text-gray-400 uppercase tracking-wider w-
|
|
44
|
+
<span class="text-[10px] font-medium text-gray-400 uppercase tracking-wider w-20 shrink-0"><%= view.h(title) %></span>
|
|
40
45
|
<% counts.sort_by { |_value, count| -count }.first(8).each do |value, count| -%>
|
|
41
46
|
<span class="badge badge-neutral"><%= view.h(value) %> <span class="tabular-nums">×<%= count %></span></span>
|
|
42
47
|
<% end -%>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sentiero
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0.
|
|
4
|
+
version: 1.0.0.beta3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stephen Ierodiaconou
|
|
@@ -117,6 +117,7 @@ files:
|
|
|
117
117
|
- lib/sentiero/stores/redis/keys.rb
|
|
118
118
|
- lib/sentiero/stores/redis/lua.rb
|
|
119
119
|
- lib/sentiero/stores/sqlite.rb
|
|
120
|
+
- lib/sentiero/stores/sqlite/payload_codec.rb
|
|
120
121
|
- lib/sentiero/stores/sqlite/schema.rb
|
|
121
122
|
- lib/sentiero/user_agent.rb
|
|
122
123
|
- lib/sentiero/version.rb
|