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.
Files changed (155) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +7 -0
  3. data/README.md +679 -0
  4. data/lib/sentiero/analytics/analyzer.rb +91 -0
  5. data/lib/sentiero/analytics/bounded.rb +29 -0
  6. data/lib/sentiero/analytics/browser_event_discovery.rb +70 -0
  7. data/lib/sentiero/analytics/collectors/click_collector.rb +135 -0
  8. data/lib/sentiero/analytics/collectors/custom_tag_collector.rb +61 -0
  9. data/lib/sentiero/analytics/collectors/error_collector.rb +89 -0
  10. data/lib/sentiero/analytics/collectors/form_collector.rb +156 -0
  11. data/lib/sentiero/analytics/collectors/frustration_collector.rb +85 -0
  12. data/lib/sentiero/analytics/collectors/scroll_collector.rb +156 -0
  13. data/lib/sentiero/analytics/collectors/vitals_collector.rb +104 -0
  14. data/lib/sentiero/analytics/conversion_analyzer.rb +247 -0
  15. data/lib/sentiero/analytics/engagement_analyzer.rb +331 -0
  16. data/lib/sentiero/analytics/entry_attribution.rb +71 -0
  17. data/lib/sentiero/analytics/error_discovery.rb +118 -0
  18. data/lib/sentiero/analytics/events.rb +21 -0
  19. data/lib/sentiero/analytics/exporter.rb +242 -0
  20. data/lib/sentiero/analytics/form_analyzer.rb +153 -0
  21. data/lib/sentiero/analytics/frustration/detectors.rb +158 -0
  22. data/lib/sentiero/analytics/frustration_analyzer.rb +235 -0
  23. data/lib/sentiero/analytics/funnel_analyzer.rb +160 -0
  24. data/lib/sentiero/analytics/heatmap_analyzer.rb +93 -0
  25. data/lib/sentiero/analytics/page_report_analyzer.rb +198 -0
  26. data/lib/sentiero/analytics/problem_detail.rb +97 -0
  27. data/lib/sentiero/analytics/scroll_depth_analyzer.rb +30 -0
  28. data/lib/sentiero/analytics/segmenter.rb +133 -0
  29. data/lib/sentiero/analytics/server_event_metrics.rb +120 -0
  30. data/lib/sentiero/analytics/stats.rb +30 -0
  31. data/lib/sentiero/analytics/stats_aggregator/result_builder.rb +153 -0
  32. data/lib/sentiero/analytics/stats_aggregator.rb +346 -0
  33. data/lib/sentiero/analytics/web_vitals_analyzer.rb +57 -0
  34. data/lib/sentiero/configuration.rb +184 -0
  35. data/lib/sentiero/erasure.rb +48 -0
  36. data/lib/sentiero/fingerprint.rb +34 -0
  37. data/lib/sentiero/ip_anonymizer.rb +29 -0
  38. data/lib/sentiero/redaction/config.rb +61 -0
  39. data/lib/sentiero/redaction.rb +207 -0
  40. data/lib/sentiero/reporter/configuration.rb +50 -0
  41. data/lib/sentiero/reporter/context.rb +31 -0
  42. data/lib/sentiero/reporter/dispatcher.rb +91 -0
  43. data/lib/sentiero/reporter/http_transport.rb +57 -0
  44. data/lib/sentiero/reporter/log_transport.rb +26 -0
  45. data/lib/sentiero/reporter/middleware.rb +62 -0
  46. data/lib/sentiero/reporter/normalizer.rb +14 -0
  47. data/lib/sentiero/reporter/null_transport.rb +18 -0
  48. data/lib/sentiero/reporter/report_context.rb +29 -0
  49. data/lib/sentiero/reporter/scrubber.rb +47 -0
  50. data/lib/sentiero/reporter/test_helper.rb +32 -0
  51. data/lib/sentiero/reporter/test_transport.rb +28 -0
  52. data/lib/sentiero/reporter.rb +214 -0
  53. data/lib/sentiero/roda.rb +47 -0
  54. data/lib/sentiero/store/error_store.rb +220 -0
  55. data/lib/sentiero/store/limits.rb +31 -0
  56. data/lib/sentiero/store/session_store.rb +118 -0
  57. data/lib/sentiero/store.rb +72 -0
  58. data/lib/sentiero/stores/file.rb +566 -0
  59. data/lib/sentiero/stores/memory.rb +362 -0
  60. data/lib/sentiero/stores/redis/keys.rb +59 -0
  61. data/lib/sentiero/stores/redis/lua.rb +119 -0
  62. data/lib/sentiero/stores/redis.rb +665 -0
  63. data/lib/sentiero/stores/sqlite/schema.rb +79 -0
  64. data/lib/sentiero/stores/sqlite.rb +626 -0
  65. data/lib/sentiero/user_agent.rb +32 -0
  66. data/lib/sentiero/version.rb +5 -0
  67. data/lib/sentiero/web/analytics_app.rb +538 -0
  68. data/lib/sentiero/web/assets/analytics-RH24EOLD.js +1 -0
  69. data/lib/sentiero/web/assets/dashboard-JFYNHZZV.js +3 -0
  70. data/lib/sentiero/web/assets/heatmap-EBKFWSKN.js +1 -0
  71. data/lib/sentiero/web/assets/import-HIMBJJ4S.js +1 -0
  72. data/lib/sentiero/web/assets/manifest.json +11 -0
  73. data/lib/sentiero/web/assets/recorder-SLLXSUUX.js +71 -0
  74. data/lib/sentiero/web/assets/rrweb-player-cd435a95.js +126 -0
  75. data/lib/sentiero/web/assets/rrweb-player-css-ce5e9629.css +2 -0
  76. data/lib/sentiero/web/assets/sessions_index-2RAGTEZM.js +1 -0
  77. data/lib/sentiero/web/assets/style-d71e72fd.css +2 -0
  78. data/lib/sentiero/web/assets_app.rb +42 -0
  79. data/lib/sentiero/web/base_app.rb +319 -0
  80. data/lib/sentiero/web/basic_auth.rb +27 -0
  81. data/lib/sentiero/web/basic_auth_check.rb +41 -0
  82. data/lib/sentiero/web/body_reader.rb +44 -0
  83. data/lib/sentiero/web/csv_writer.rb +45 -0
  84. data/lib/sentiero/web/dashboard_app.rb +236 -0
  85. data/lib/sentiero/web/errors_app.rb +97 -0
  86. data/lib/sentiero/web/escaping.rb +37 -0
  87. data/lib/sentiero/web/events_app.rb +196 -0
  88. data/lib/sentiero/web/formatting.rb +43 -0
  89. data/lib/sentiero/web/ingest_app.rb +92 -0
  90. data/lib/sentiero/web/manifest.rb +43 -0
  91. data/lib/sentiero/web/monitoring_app.rb +316 -0
  92. data/lib/sentiero/web/script_tag.rb +57 -0
  93. data/lib/sentiero/web/shareable_replay.rb +88 -0
  94. data/lib/sentiero/web/templates/_analytics_nav.html.erb +22 -0
  95. data/lib/sentiero/web/templates/_brand.html.erb +18 -0
  96. data/lib/sentiero/web/templates/_date_range.html.erb +18 -0
  97. data/lib/sentiero/web/templates/_errors_client_filter.html.erb +25 -0
  98. data/lib/sentiero/web/templates/_errors_server_filter.html.erb +36 -0
  99. data/lib/sentiero/web/templates/_events_browser_filter.html.erb +18 -0
  100. data/lib/sentiero/web/templates/_events_server_filter.html.erb +39 -0
  101. data/lib/sentiero/web/templates/_pagination.html.erb +14 -0
  102. data/lib/sentiero/web/templates/_payload_metrics.html.erb +62 -0
  103. data/lib/sentiero/web/templates/_session_row.html.erb +42 -0
  104. data/lib/sentiero/web/templates/_sibling_tab_hint.html.erb +6 -0
  105. data/lib/sentiero/web/templates/_tabs.html.erb +10 -0
  106. data/lib/sentiero/web/templates/_truncation_warning.html.erb +19 -0
  107. data/lib/sentiero/web/templates/_window_tab.html.erb +5 -0
  108. data/lib/sentiero/web/templates/analytics_conversions.html.erb +94 -0
  109. data/lib/sentiero/web/templates/analytics_engagement.html.erb +101 -0
  110. data/lib/sentiero/web/templates/analytics_frustration.html.erb +135 -0
  111. data/lib/sentiero/web/templates/analytics_funnel.html.erb +103 -0
  112. data/lib/sentiero/web/templates/analytics_index.html.erb +380 -0
  113. data/lib/sentiero/web/templates/analytics_page.html.erb +287 -0
  114. data/lib/sentiero/web/templates/analytics_scroll.html.erb +94 -0
  115. data/lib/sentiero/web/templates/analytics_vitals.html.erb +91 -0
  116. data/lib/sentiero/web/templates/client_error_show.html.erb +73 -0
  117. data/lib/sentiero/web/templates/dashboard.html.erb +56 -0
  118. data/lib/sentiero/web/templates/errors_index.html.erb +149 -0
  119. data/lib/sentiero/web/templates/event_show.html.erb +52 -0
  120. data/lib/sentiero/web/templates/events_index.html.erb +177 -0
  121. data/lib/sentiero/web/templates/export_index.html.erb +69 -0
  122. data/lib/sentiero/web/templates/forms.html.erb +105 -0
  123. data/lib/sentiero/web/templates/heatmap.html.erb +76 -0
  124. data/lib/sentiero/web/templates/import.html.erb +39 -0
  125. data/lib/sentiero/web/templates/problem_show.html.erb +200 -0
  126. data/lib/sentiero/web/templates/segments.html.erb +114 -0
  127. data/lib/sentiero/web/templates/session_show.html.erb +195 -0
  128. data/lib/sentiero/web/templates/sessions_index.html.erb +97 -0
  129. data/lib/sentiero/web/track_app.rb +57 -0
  130. data/lib/sentiero/web/views/analytics_index_view.rb +86 -0
  131. data/lib/sentiero/web/views/analyzer_view.rb +27 -0
  132. data/lib/sentiero/web/views/base_view.rb +76 -0
  133. data/lib/sentiero/web/views/client_error_show_view.rb +29 -0
  134. data/lib/sentiero/web/views/conversions_view.rb +41 -0
  135. data/lib/sentiero/web/views/engagement_view.rb +67 -0
  136. data/lib/sentiero/web/views/errors_index_view.rb +37 -0
  137. data/lib/sentiero/web/views/event_show_view.rb +20 -0
  138. data/lib/sentiero/web/views/events_index_view.rb +56 -0
  139. data/lib/sentiero/web/views/export_view.rb +23 -0
  140. data/lib/sentiero/web/views/forms_view.rb +28 -0
  141. data/lib/sentiero/web/views/frustration_view.rb +15 -0
  142. data/lib/sentiero/web/views/funnel_view.rb +36 -0
  143. data/lib/sentiero/web/views/heatmap_view.rb +34 -0
  144. data/lib/sentiero/web/views/import_view.rb +13 -0
  145. data/lib/sentiero/web/views/page_report_view.rb +43 -0
  146. data/lib/sentiero/web/views/problem_show_view.rb +46 -0
  147. data/lib/sentiero/web/views/scroll_view.rb +23 -0
  148. data/lib/sentiero/web/views/segments_view.rb +28 -0
  149. data/lib/sentiero/web/views/session_show_view.rb +105 -0
  150. data/lib/sentiero/web/views/sessions_index_view.rb +28 -0
  151. data/lib/sentiero/web/views/vitals_view.rb +45 -0
  152. data/lib/sentiero/web/views.rb +24 -0
  153. data/lib/sentiero/window_ref.rb +6 -0
  154. data/lib/sentiero.rb +69 -0
  155. 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