sentiero 1.0.0.alpha1
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 +7 -0
- data/LICENSE.txt +7 -0
- data/README.md +679 -0
- data/lib/sentiero/analytics/analyzer.rb +91 -0
- data/lib/sentiero/analytics/bounded.rb +29 -0
- data/lib/sentiero/analytics/browser_event_discovery.rb +70 -0
- data/lib/sentiero/analytics/collectors/click_collector.rb +135 -0
- data/lib/sentiero/analytics/collectors/custom_tag_collector.rb +61 -0
- data/lib/sentiero/analytics/collectors/error_collector.rb +89 -0
- data/lib/sentiero/analytics/collectors/form_collector.rb +156 -0
- data/lib/sentiero/analytics/collectors/frustration_collector.rb +85 -0
- data/lib/sentiero/analytics/collectors/scroll_collector.rb +156 -0
- data/lib/sentiero/analytics/collectors/vitals_collector.rb +104 -0
- data/lib/sentiero/analytics/conversion_analyzer.rb +247 -0
- data/lib/sentiero/analytics/engagement_analyzer.rb +331 -0
- data/lib/sentiero/analytics/entry_attribution.rb +71 -0
- data/lib/sentiero/analytics/error_discovery.rb +118 -0
- data/lib/sentiero/analytics/events.rb +21 -0
- data/lib/sentiero/analytics/exporter.rb +242 -0
- data/lib/sentiero/analytics/form_analyzer.rb +153 -0
- data/lib/sentiero/analytics/frustration/detectors.rb +158 -0
- data/lib/sentiero/analytics/frustration_analyzer.rb +235 -0
- data/lib/sentiero/analytics/funnel_analyzer.rb +160 -0
- data/lib/sentiero/analytics/heatmap_analyzer.rb +93 -0
- data/lib/sentiero/analytics/page_report_analyzer.rb +198 -0
- data/lib/sentiero/analytics/problem_detail.rb +97 -0
- data/lib/sentiero/analytics/scroll_depth_analyzer.rb +30 -0
- data/lib/sentiero/analytics/segmenter.rb +133 -0
- data/lib/sentiero/analytics/server_event_metrics.rb +120 -0
- data/lib/sentiero/analytics/stats.rb +30 -0
- data/lib/sentiero/analytics/stats_aggregator/result_builder.rb +153 -0
- data/lib/sentiero/analytics/stats_aggregator.rb +346 -0
- data/lib/sentiero/analytics/web_vitals_analyzer.rb +57 -0
- data/lib/sentiero/configuration.rb +184 -0
- data/lib/sentiero/erasure.rb +48 -0
- data/lib/sentiero/fingerprint.rb +34 -0
- data/lib/sentiero/ip_anonymizer.rb +29 -0
- data/lib/sentiero/redaction/config.rb +61 -0
- data/lib/sentiero/redaction.rb +207 -0
- data/lib/sentiero/reporter/configuration.rb +50 -0
- data/lib/sentiero/reporter/context.rb +31 -0
- data/lib/sentiero/reporter/dispatcher.rb +91 -0
- data/lib/sentiero/reporter/http_transport.rb +57 -0
- data/lib/sentiero/reporter/log_transport.rb +26 -0
- data/lib/sentiero/reporter/middleware.rb +62 -0
- data/lib/sentiero/reporter/normalizer.rb +14 -0
- data/lib/sentiero/reporter/null_transport.rb +18 -0
- data/lib/sentiero/reporter/report_context.rb +29 -0
- data/lib/sentiero/reporter/scrubber.rb +47 -0
- data/lib/sentiero/reporter/test_helper.rb +32 -0
- data/lib/sentiero/reporter/test_transport.rb +28 -0
- data/lib/sentiero/reporter.rb +214 -0
- data/lib/sentiero/roda.rb +47 -0
- data/lib/sentiero/store/error_store.rb +220 -0
- data/lib/sentiero/store/limits.rb +31 -0
- data/lib/sentiero/store/session_store.rb +118 -0
- data/lib/sentiero/store.rb +72 -0
- data/lib/sentiero/stores/file.rb +566 -0
- data/lib/sentiero/stores/memory.rb +362 -0
- data/lib/sentiero/stores/redis/keys.rb +59 -0
- data/lib/sentiero/stores/redis/lua.rb +119 -0
- data/lib/sentiero/stores/redis.rb +665 -0
- data/lib/sentiero/stores/sqlite/schema.rb +79 -0
- data/lib/sentiero/stores/sqlite.rb +626 -0
- data/lib/sentiero/user_agent.rb +32 -0
- data/lib/sentiero/version.rb +5 -0
- data/lib/sentiero/web/analytics_app.rb +538 -0
- data/lib/sentiero/web/assets/analytics-RH24EOLD.js +1 -0
- data/lib/sentiero/web/assets/dashboard-JFYNHZZV.js +3 -0
- data/lib/sentiero/web/assets/heatmap-EBKFWSKN.js +1 -0
- data/lib/sentiero/web/assets/import-HIMBJJ4S.js +1 -0
- data/lib/sentiero/web/assets/manifest.json +11 -0
- data/lib/sentiero/web/assets/recorder-SLLXSUUX.js +71 -0
- data/lib/sentiero/web/assets/rrweb-player-cd435a95.js +126 -0
- data/lib/sentiero/web/assets/rrweb-player-css-ce5e9629.css +2 -0
- data/lib/sentiero/web/assets/sessions_index-2RAGTEZM.js +1 -0
- data/lib/sentiero/web/assets/style-d71e72fd.css +2 -0
- data/lib/sentiero/web/assets_app.rb +42 -0
- data/lib/sentiero/web/base_app.rb +319 -0
- data/lib/sentiero/web/basic_auth.rb +27 -0
- data/lib/sentiero/web/basic_auth_check.rb +41 -0
- data/lib/sentiero/web/body_reader.rb +44 -0
- data/lib/sentiero/web/csv_writer.rb +45 -0
- data/lib/sentiero/web/dashboard_app.rb +236 -0
- data/lib/sentiero/web/errors_app.rb +97 -0
- data/lib/sentiero/web/escaping.rb +37 -0
- data/lib/sentiero/web/events_app.rb +196 -0
- data/lib/sentiero/web/formatting.rb +43 -0
- data/lib/sentiero/web/ingest_app.rb +92 -0
- data/lib/sentiero/web/manifest.rb +43 -0
- data/lib/sentiero/web/monitoring_app.rb +316 -0
- data/lib/sentiero/web/script_tag.rb +57 -0
- data/lib/sentiero/web/shareable_replay.rb +88 -0
- data/lib/sentiero/web/templates/_analytics_nav.html.erb +22 -0
- data/lib/sentiero/web/templates/_brand.html.erb +18 -0
- data/lib/sentiero/web/templates/_date_range.html.erb +18 -0
- data/lib/sentiero/web/templates/_errors_client_filter.html.erb +25 -0
- data/lib/sentiero/web/templates/_errors_server_filter.html.erb +36 -0
- data/lib/sentiero/web/templates/_events_browser_filter.html.erb +18 -0
- data/lib/sentiero/web/templates/_events_server_filter.html.erb +39 -0
- data/lib/sentiero/web/templates/_pagination.html.erb +14 -0
- data/lib/sentiero/web/templates/_payload_metrics.html.erb +62 -0
- data/lib/sentiero/web/templates/_session_row.html.erb +42 -0
- data/lib/sentiero/web/templates/_sibling_tab_hint.html.erb +6 -0
- data/lib/sentiero/web/templates/_tabs.html.erb +10 -0
- data/lib/sentiero/web/templates/_truncation_warning.html.erb +19 -0
- data/lib/sentiero/web/templates/_window_tab.html.erb +5 -0
- data/lib/sentiero/web/templates/analytics_conversions.html.erb +94 -0
- data/lib/sentiero/web/templates/analytics_engagement.html.erb +101 -0
- data/lib/sentiero/web/templates/analytics_frustration.html.erb +135 -0
- data/lib/sentiero/web/templates/analytics_funnel.html.erb +103 -0
- data/lib/sentiero/web/templates/analytics_index.html.erb +380 -0
- data/lib/sentiero/web/templates/analytics_page.html.erb +287 -0
- data/lib/sentiero/web/templates/analytics_scroll.html.erb +94 -0
- data/lib/sentiero/web/templates/analytics_vitals.html.erb +91 -0
- data/lib/sentiero/web/templates/client_error_show.html.erb +73 -0
- data/lib/sentiero/web/templates/dashboard.html.erb +56 -0
- data/lib/sentiero/web/templates/errors_index.html.erb +149 -0
- data/lib/sentiero/web/templates/event_show.html.erb +52 -0
- data/lib/sentiero/web/templates/events_index.html.erb +177 -0
- data/lib/sentiero/web/templates/export_index.html.erb +69 -0
- data/lib/sentiero/web/templates/forms.html.erb +105 -0
- data/lib/sentiero/web/templates/heatmap.html.erb +76 -0
- data/lib/sentiero/web/templates/import.html.erb +39 -0
- data/lib/sentiero/web/templates/problem_show.html.erb +200 -0
- data/lib/sentiero/web/templates/segments.html.erb +114 -0
- data/lib/sentiero/web/templates/session_show.html.erb +195 -0
- data/lib/sentiero/web/templates/sessions_index.html.erb +97 -0
- data/lib/sentiero/web/track_app.rb +57 -0
- data/lib/sentiero/web/views/analytics_index_view.rb +86 -0
- data/lib/sentiero/web/views/analyzer_view.rb +27 -0
- data/lib/sentiero/web/views/base_view.rb +76 -0
- data/lib/sentiero/web/views/client_error_show_view.rb +29 -0
- data/lib/sentiero/web/views/conversions_view.rb +41 -0
- data/lib/sentiero/web/views/engagement_view.rb +67 -0
- data/lib/sentiero/web/views/errors_index_view.rb +37 -0
- data/lib/sentiero/web/views/event_show_view.rb +20 -0
- data/lib/sentiero/web/views/events_index_view.rb +56 -0
- data/lib/sentiero/web/views/export_view.rb +23 -0
- data/lib/sentiero/web/views/forms_view.rb +28 -0
- data/lib/sentiero/web/views/frustration_view.rb +15 -0
- data/lib/sentiero/web/views/funnel_view.rb +36 -0
- data/lib/sentiero/web/views/heatmap_view.rb +34 -0
- data/lib/sentiero/web/views/import_view.rb +13 -0
- data/lib/sentiero/web/views/page_report_view.rb +43 -0
- data/lib/sentiero/web/views/problem_show_view.rb +46 -0
- data/lib/sentiero/web/views/scroll_view.rb +23 -0
- data/lib/sentiero/web/views/segments_view.rb +28 -0
- data/lib/sentiero/web/views/session_show_view.rb +105 -0
- data/lib/sentiero/web/views/sessions_index_view.rb +28 -0
- data/lib/sentiero/web/views/vitals_view.rb +45 -0
- data/lib/sentiero/web/views.rb +24 -0
- data/lib/sentiero/window_ref.rb +6 -0
- data/lib/sentiero.rb +69 -0
- metadata +232 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sentiero
|
|
4
|
+
class Store
|
|
5
|
+
# The session-replay store contract: recording windows of
|
|
6
|
+
# rrweb events and reading them back as sessions.
|
|
7
|
+
#
|
|
8
|
+
# Window-level methods take a Sentiero::WindowRef; session-level methods
|
|
9
|
+
# take a bare session_id.
|
|
10
|
+
module SessionStore
|
|
11
|
+
def save_events(ref, events)
|
|
12
|
+
raise NoMethodError, "#{self.class}#save_events not implemented"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def list_sessions(limit:, offset: 0, since: nil, until_time: nil, sort_by: nil, search: nil)
|
|
16
|
+
raise NoMethodError, "#{self.class}#list_sessions not implemented"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_session(session_id)
|
|
20
|
+
raise NoMethodError, "#{self.class}#get_session not implemented"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def get_events(ref, after: nil, limit: nil)
|
|
24
|
+
raise NoMethodError, "#{self.class}#get_events not implemented"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def delete_session(session_id)
|
|
28
|
+
raise NoMethodError, "#{self.class}#delete_session not implemented"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete_window(ref)
|
|
32
|
+
raise NoMethodError, "#{self.class}#delete_window not implemented"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Optional; default is a no-op so custom stores keep working without it.
|
|
36
|
+
def save_metadata(session_id, metadata)
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Yields [session_summary_hash, window_id, events_array] per window, newest
|
|
41
|
+
# sessions first, capped at `limit`. Built from list_sessions/get_session/
|
|
42
|
+
# get_events so every backend gets it free; stores may override.
|
|
43
|
+
def each_session_events(limit: nil, since: nil, until_time: nil)
|
|
44
|
+
return enum_for(:each_session_events, limit: limit, since: since, until_time: until_time) unless block_given?
|
|
45
|
+
|
|
46
|
+
cap = limit || limits.analytics_max_scan_sessions
|
|
47
|
+
sessions = list_sessions(limit: cap, since: since, until_time: until_time)
|
|
48
|
+
|
|
49
|
+
sessions.each do |summary|
|
|
50
|
+
session = get_session(summary[:session_id])
|
|
51
|
+
next unless session
|
|
52
|
+
|
|
53
|
+
windows = session[:windows] || []
|
|
54
|
+
windows.each do |window|
|
|
55
|
+
window_id = window[:window_id]
|
|
56
|
+
events = get_events(WindowRef.new(summary[:session_id], window_id))
|
|
57
|
+
yield summary, window_id, events
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Deletes every session whose updated_at is older than `seconds` ago,
|
|
63
|
+
# returning the count. Built from list_sessions + delete_session so every
|
|
64
|
+
# backend gets it free; stores may override with a direct query.
|
|
65
|
+
#
|
|
66
|
+
# list_sessions is newest-first, so stale sessions are the last ones
|
|
67
|
+
# reached: we page through the whole store by advancing an offset (not just
|
|
68
|
+
# re-reading the first batch) and delete only after the full scan, so
|
|
69
|
+
# deletions don't shift the pages we're still walking.
|
|
70
|
+
def purge_older_than(seconds)
|
|
71
|
+
cutoff = Time.now.to_f - seconds
|
|
72
|
+
batch_size = limits.analytics_max_scan_sessions
|
|
73
|
+
stale = []
|
|
74
|
+
offset = 0
|
|
75
|
+
|
|
76
|
+
loop do
|
|
77
|
+
summaries = list_sessions(limit: batch_size, offset: offset)
|
|
78
|
+
break if summaries.empty?
|
|
79
|
+
|
|
80
|
+
stale.concat(
|
|
81
|
+
summaries
|
|
82
|
+
.select { |summary| summary[:updated_at] < cutoff }
|
|
83
|
+
.map { |summary| summary[:session_id] }
|
|
84
|
+
)
|
|
85
|
+
break if summaries.size < batch_size
|
|
86
|
+
|
|
87
|
+
offset += batch_size
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
stale.each { |session_id| delete_session(session_id) }
|
|
91
|
+
stale.size
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# The session-summary shape returned by list_sessions/each_session_events,
|
|
97
|
+
# shared by every backend so the seven near-identical hash literals stay
|
|
98
|
+
# in exact lockstep. metadata is included only when present and non-empty,
|
|
99
|
+
# matching how each backend already treats "no metadata" as "no key".
|
|
100
|
+
def summary_hash(session_id:, window_ids:, event_count:, created_at:, updated_at:,
|
|
101
|
+
first_event_at: nil, last_event_at: nil, metadata: nil)
|
|
102
|
+
entry = {session_id: session_id, window_ids: window_ids, event_count: event_count,
|
|
103
|
+
created_at: created_at, updated_at: updated_at,
|
|
104
|
+
first_event_at: first_event_at, last_event_at: last_event_at}
|
|
105
|
+
entry[:metadata] = metadata if metadata && !metadata.empty?
|
|
106
|
+
entry
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# True when the search term (case-insensitive) appears in the session_id
|
|
110
|
+
# or any metadata value.
|
|
111
|
+
def session_matches_search?(summary, search)
|
|
112
|
+
search_down = search.downcase
|
|
113
|
+
summary[:session_id].downcase.include?(search_down) ||
|
|
114
|
+
summary[:metadata]&.values&.any? { |value| value.to_s.downcase.include?(search_down) } || false
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "window_ref"
|
|
4
|
+
require_relative "store/limits"
|
|
5
|
+
require_relative "store/session_store"
|
|
6
|
+
require_relative "store/error_store"
|
|
7
|
+
|
|
8
|
+
module Sentiero
|
|
9
|
+
# Abstract store contract, split across two mixins: SessionStore
|
|
10
|
+
# (session-replay recording) and ErrorStore (error tracking). Concrete
|
|
11
|
+
# backends subclass Store and implement both halves.
|
|
12
|
+
class Store
|
|
13
|
+
VALID_ID = /\A[a-zA-Z0-9_-]{1,128}\z/
|
|
14
|
+
MAX_METADATA_KEYS = 50
|
|
15
|
+
MAX_METADATA_VALUE_SIZE = 1024
|
|
16
|
+
VALID_STATUS = %w[open resolved ignored].freeze
|
|
17
|
+
PROBLEM_TITLE_MAX = 200
|
|
18
|
+
|
|
19
|
+
include SessionStore
|
|
20
|
+
include ErrorStore
|
|
21
|
+
|
|
22
|
+
attr_writer :limits
|
|
23
|
+
|
|
24
|
+
# Caps for eviction/scans. Defaults to Limits::DEFAULTS (static, not the
|
|
25
|
+
# global config); pass limits: Limits.from_configuration to bind it, or
|
|
26
|
+
# inject any other Limits to decouple a store from global state.
|
|
27
|
+
def limits
|
|
28
|
+
@limits ||= Limits.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def validate_id!(id)
|
|
34
|
+
raise ArgumentError, "Invalid ID: #{id.inspect}" unless VALID_ID.match?(id.to_s)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_window_ref!(ref)
|
|
38
|
+
validate_id!(ref.session_id)
|
|
39
|
+
validate_id!(ref.window_id)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate_metadata!(metadata)
|
|
43
|
+
raise ArgumentError, "metadata must be a Hash" unless metadata.is_a?(Hash)
|
|
44
|
+
metadata.each do |key, value|
|
|
45
|
+
raise ArgumentError, "metadata key too long" if key.to_s.length > 128
|
|
46
|
+
raise ArgumentError, "metadata value too large" if value.to_s.length > MAX_METADATA_VALUE_SIZE
|
|
47
|
+
end
|
|
48
|
+
raise ArgumentError, "too many metadata keys" if metadata.size > MAX_METADATA_KEYS
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_status!(status)
|
|
52
|
+
raise ArgumentError, "Invalid status: #{status.inspect}" unless VALID_STATUS.include?(status)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate_occurrence!(occurrence)
|
|
56
|
+
raise ArgumentError, "occurrence must be a Hash" unless occurrence.is_a?(Hash)
|
|
57
|
+
%w[fingerprint project exception_class message timestamp].each do |key|
|
|
58
|
+
raise ArgumentError, "occurrence missing #{key}" if occurrence[key].nil?
|
|
59
|
+
end
|
|
60
|
+
validate_id!(occurrence["fingerprint"])
|
|
61
|
+
validate_id!(occurrence["project"])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_server_event!(event)
|
|
65
|
+
raise ArgumentError, "server event must be a Hash" unless event.is_a?(Hash)
|
|
66
|
+
%w[project name timestamp].each do |key|
|
|
67
|
+
raise ArgumentError, "server event missing #{key}" if event[key].nil?
|
|
68
|
+
end
|
|
69
|
+
validate_id!(event["project"])
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|