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,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_view"
4
+
5
+ module Sentiero
6
+ module Web
7
+ module Views
8
+ class FunnelView < BaseView
9
+ def initialize(tags:, selected_steps:, steps:, was_truncated:, since:, until_str:)
10
+ super()
11
+ @tags = tags
12
+ @selected_steps = selected_steps
13
+ @steps = steps
14
+ @was_truncated = was_truncated
15
+ @since = since
16
+ @until_str = until_str
17
+ end
18
+
19
+ attr_reader :tags, :selected_steps, :steps, :was_truncated, :since, :until_str
20
+
21
+ def template = "analytics_funnel.html.erb"
22
+
23
+ def format_gap(ms) = (ms < 1000) ? "#{ms.round}ms" : format_duration(0, ms)
24
+
25
+ def step_one_sessions = @step_one_sessions ||= steps.first&.fetch(:sessions).to_i
26
+
27
+ def next_step(i) = steps[i + 1]
28
+
29
+ def dropped(i)
30
+ n = next_step(i)
31
+ n ? steps[i][:sessions] - n[:sessions] : 0
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require_relative "base_view"
5
+
6
+ module Sentiero
7
+ module Web
8
+ module Views
9
+ class HeatmapView < BaseView
10
+ def initialize(urls:, selected_url:, was_truncated:, config_json:, since:, until_str:)
11
+ super()
12
+ @urls = urls
13
+ @selected_url = selected_url
14
+ @was_truncated = was_truncated
15
+ @config_json = config_json
16
+ @since = since
17
+ @until_str = until_str
18
+ end
19
+
20
+ attr_reader :urls, :selected_url, :was_truncated, :config_json, :since, :until_str
21
+
22
+ def template = "heatmap.html.erb"
23
+
24
+ # Query holds server-side strings only; escaped at the h.call sink.
25
+ def page_report_href
26
+ q = {}
27
+ q["url"] = selected_url if selected_url && !selected_url.to_s.empty?
28
+ q.merge!(range_pairs)
29
+ "#{base_path}/analytics/page" + (q.empty? ? "" : "?" + Rack::Utils.build_query(q))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_view"
4
+
5
+ module Sentiero
6
+ module Web
7
+ module Views
8
+ class ImportView < BaseView
9
+ def template = "import.html.erb"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack"
4
+ require_relative "base_view"
5
+
6
+ module Sentiero
7
+ module Web
8
+ module Views
9
+ class PageReportView < BaseView
10
+ def initialize(report:, urls:, selected_url:, was_truncated:, since:, until_str:)
11
+ super()
12
+ @report = report
13
+ @urls = urls
14
+ @selected_url = selected_url
15
+ @was_truncated = was_truncated
16
+ @since = since
17
+ @until_str = until_str
18
+ end
19
+
20
+ attr_reader :report, :urls, :selected_url, :was_truncated, :since, :until_str
21
+
22
+ def template = "analytics_page.html.erb"
23
+
24
+ def secs(ms) = ms ? "#{(ms / 1000.0).round(1)}s" : "&mdash;"
25
+
26
+ def pct(rate) = "#{(rate * 100).round(1)}%"
27
+
28
+ def heatmap_href
29
+ q = {}
30
+ q["url"] = selected_url if selected_url && !selected_url.to_s.empty?
31
+ q.merge!(range_pairs)
32
+ "#{base_path}/analytics/heatmap" + (q.empty? ? "" : "?" + Rack::Utils.build_query(q))
33
+ end
34
+
35
+ def time_on_page = report[:time_on_page]
36
+ def entry_exit = report[:entry_exit]
37
+ def scroll = report[:scroll]
38
+ def forms = report[:forms]
39
+ def frustration = report[:frustration]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_view"
4
+
5
+ module Sentiero
6
+ module Web
7
+ module Views
8
+ class ProblemShowView < BaseView
9
+ def initialize(problem:, occurrences:, session_ids:, session_summaries:, facets:, trend:)
10
+ super()
11
+ @problem = problem
12
+ @occurrences = occurrences
13
+ @session_ids = session_ids
14
+ @session_summaries = session_summaries
15
+ @facets = facets
16
+ @trend = trend
17
+ end
18
+
19
+ attr_reader :problem, :occurrences, :session_ids, :session_summaries, :facets, :trend
20
+
21
+ def template = "problem_show.html.erb"
22
+
23
+ def replay_href(s)
24
+ sid = s[:session_id]
25
+ if s[:window_id]
26
+ "#{h(base_path)}/sessions/#{h(sid)}/windows/#{h(s[:window_id])}"
27
+ else
28
+ "#{h(base_path)}/sessions/#{h(sid)}"
29
+ end
30
+ end
31
+
32
+ def spark_max = trend[:series].map { |day| day[:count] }.max || 0
33
+
34
+ def facet_groups
35
+ [
36
+ ["Top Request Paths", facets[:paths]],
37
+ ["Environments", facets[:environments]],
38
+ ["Browsers", facets[:browsers]]
39
+ ]
40
+ end
41
+
42
+ def has_facets? = facets[:releases].any? || facet_groups.any? { |_title, rows| rows.any? }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "analyzer_view"
4
+
5
+ module Sentiero
6
+ module Web
7
+ module Views
8
+ class ScrollView < AnalyzerView
9
+ def template = "analytics_scroll.html.erb"
10
+
11
+ def sorted_pages = pages.sort_by { |url, page| [-page[:session_count], url] }
12
+
13
+ def svg_width = 320
14
+ def svg_height = 140
15
+ def bar_gap = 12
16
+ def axis_y = svg_height - 24
17
+ def chart_top = 12
18
+ def bar_width = (svg_width - bar_gap * 5) / 4
19
+ def max_count(dist) = [dist.values.max, 1].max
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_view"
4
+
5
+ module Sentiero
6
+ module Web
7
+ module Views
8
+ class SegmentsView < BaseView
9
+ def initialize(filters:, browser_options:, device_options:, sessions:, page:, per_page:, has_next:, was_truncated:, filter_query:)
10
+ super()
11
+ @filters = filters
12
+ @browser_options = browser_options
13
+ @device_options = device_options
14
+ @sessions = sessions
15
+ @page = page
16
+ @per_page = per_page
17
+ @has_next = has_next
18
+ @was_truncated = was_truncated
19
+ @filter_query = filter_query
20
+ end
21
+
22
+ attr_reader :filters, :browser_options, :device_options, :sessions, :page, :per_page, :has_next, :was_truncated, :filter_query
23
+
24
+ def template = "segments.html.erb"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_view"
4
+
5
+ module Sentiero
6
+ module Web
7
+ module Views
8
+ class SessionShowView < BaseView
9
+ # Maximum tab buttons shown inline in the window switcher before the rest
10
+ # collapse into the overflow menu.
11
+ MAX_VISIBLE_TABS = 5
12
+
13
+ def initialize(session:, session_id:, window_id:, shareable_replays:, server_activity:)
14
+ super()
15
+ @session = session
16
+ @session_id = session_id
17
+ @window_id = window_id
18
+ @shareable_replays = shareable_replays
19
+ @server_activity = server_activity
20
+ end
21
+
22
+ attr_reader :session, :session_id, :window_id, :shareable_replays, :server_activity
23
+
24
+ def template = "session_show.html.erb"
25
+
26
+ def meta = session[:metadata]
27
+ def total_events = session[:windows]&.sum { |w| w[:event_count] } || 0
28
+ def duration_str = format_duration(session[:first_event_at], session[:last_event_at])
29
+ def multi_window? = tabs[:all].size > 1
30
+ def custom_keys = meta.keys - ["url", "referrer", "userAgent", "viewport"]
31
+
32
+ # Splits the session's windows into the switcher's inline tab buttons and
33
+ # overflow menu, keeping the active window visible. Windows are sorted by
34
+ # last activity and numbered once into {window:, tab_num:} pairs so the
35
+ # template never re-derives a tab number. Returns {all:, visible:, overflow:}.
36
+ def tabs
37
+ @tabs ||= partition_windows(session[:windows] || [], window_id)
38
+ end
39
+
40
+ # Player-relative server-activity markers, lazily so base_path (set after
41
+ # construction by render_page) is available.
42
+ def server_markers
43
+ @server_markers ||= build_server_markers
44
+ end
45
+
46
+ private
47
+
48
+ def partition_windows(windows, active_window_id, max_visible: MAX_VISIBLE_TABS)
49
+ sorted = windows.sort_by { |w| w[:last_event_at] || 0 }
50
+ tabs = sorted.each_with_index.map { |w, i| {window: w, tab_num: i + 1} }
51
+ return {all: tabs, visible: tabs, overflow: []} if tabs.size <= max_visible
52
+
53
+ # Show the first max_visible - 1 tabs (one slot is kept for the
54
+ # "+N more" button), swapping the last visible slot for the active
55
+ # tab when it would otherwise be hidden.
56
+ visible_indices = (0...(max_visible - 1)).to_a
57
+ active_idx = tabs.index { |t| t[:window][:window_id] == active_window_id }
58
+ visible_indices[-1] = active_idx if active_idx && !visible_indices.include?(active_idx)
59
+ visible = visible_indices.sort.map { |i| tabs[i] }
60
+ {all: tabs, visible: visible, overflow: tabs - visible}
61
+ end
62
+
63
+ # The player anchors t=0 at the window's first event (epoch MILLISECONDS);
64
+ # server timestamps are float SECONDS, so they are *1000 before subtracting
65
+ # the anchor. Offsets clamp to >= 0 so pre-window items pin at the start.
66
+ def build_server_markers
67
+ return [] if server_activity.nil? || server_activity.empty?
68
+
69
+ window = (session[:windows] || []).find { |w| w[:window_id] == window_id }
70
+ anchor_ms = window && window[:first_event_at]
71
+ return [] unless anchor_ms
72
+
73
+ server_activity.map { |item|
74
+ offset_ms = ((item[:timestamp] * 1000) - anchor_ms).round
75
+ offset_ms = 0 if offset_ms < 0
76
+
77
+ if item[:kind] == "exception"
78
+ occ = item[:occurrence]
79
+ cls = occ["exception_class"].to_s
80
+ msg = occ["message"].to_s
81
+ label = msg.empty? ? cls : "#{cls}: #{msg}"
82
+ {
83
+ offset_ms: offset_ms,
84
+ kind: "exception",
85
+ label: label[0, 120],
86
+ level: "error",
87
+ href: "#{base_path}/issues/#{occ["fingerprint"]}"
88
+ }
89
+ else
90
+ event = item[:event]
91
+ href = event["id"] ? "#{base_path}/custom-events/#{event["id"]}" : nil
92
+ {
93
+ offset_ms: offset_ms,
94
+ kind: "event",
95
+ label: event["name"].to_s[0, 120],
96
+ level: event["level"].to_s,
97
+ href: href
98
+ }
99
+ end
100
+ }.sort_by { |m| m[:offset_ms] }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_view"
4
+
5
+ module Sentiero
6
+ module Web
7
+ module Views
8
+ class SessionsIndexView < BaseView
9
+ def initialize(sessions:, page:, per_page:, has_next:, search:, sort_by:, since:, until_param:, has_errors:)
10
+ super()
11
+ @sessions = sessions
12
+ @page = page
13
+ @per_page = per_page
14
+ @has_next = has_next
15
+ @search = search
16
+ @sort_by = sort_by
17
+ @since = since
18
+ @until_param = until_param
19
+ @has_errors = has_errors
20
+ end
21
+
22
+ attr_reader :sessions, :page, :per_page, :has_next, :search, :sort_by, :since, :until_param, :has_errors
23
+
24
+ def template = "sessions_index.html.erb"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "analyzer_view"
4
+
5
+ module Sentiero
6
+ module Web
7
+ module Views
8
+ class VitalsView < AnalyzerView
9
+ RATING_COLORS = {"good" => "#16a34a", "needs-improvement" => "#d97706", "poor" => "#dc2626"}.freeze
10
+
11
+ def template = "analytics_vitals.html.erb"
12
+
13
+ def sorted_pages = pages.sort_by { |url, page| [-page[:sample_count], url] }
14
+
15
+ def dominant_rating(ratings) = ratings.max_by { |rating, count| [count, rating] }&.first
16
+
17
+ def rating_class(rating)
18
+ case rating
19
+ when "good" then "badge-success"
20
+ when "needs-improvement" then "badge-warning"
21
+ when "poor" then "badge-danger"
22
+ else "badge-neutral"
23
+ end
24
+ end
25
+
26
+ def rating_colors = RATING_COLORS
27
+
28
+ def page_rating_mix(page)
29
+ rating_colors.keys.to_h do |rating|
30
+ [rating, page[:metrics].sum { |_metric, m| m[:ratings].fetch(rating, 0) }]
31
+ end
32
+ end
33
+
34
+ def slowest(page)
35
+ %w[LCP INP CLS].each do |metric|
36
+ worst = page[:metrics].dig(metric, :worst)
37
+ return worst if worst
38
+ end
39
+ page[:metrics].each_value { |m| return m[:worst] if m[:worst] }
40
+ nil
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "views/base_view"
4
+ require_relative "views/funnel_view"
5
+ require_relative "views/conversions_view"
6
+ require_relative "views/engagement_view"
7
+ require_relative "views/page_report_view"
8
+ require_relative "views/analytics_index_view"
9
+ require_relative "views/segments_view"
10
+ require_relative "views/forms_view"
11
+ require_relative "views/heatmap_view"
12
+ require_relative "views/export_view"
13
+ require_relative "views/import_view"
14
+ require_relative "views/analyzer_view"
15
+ require_relative "views/scroll_view"
16
+ require_relative "views/vitals_view"
17
+ require_relative "views/frustration_view"
18
+ require_relative "views/sessions_index_view"
19
+ require_relative "views/session_show_view"
20
+ require_relative "views/event_show_view"
21
+ require_relative "views/events_index_view"
22
+ require_relative "views/errors_index_view"
23
+ require_relative "views/client_error_show_view"
24
+ require_relative "views/problem_show_view"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentiero
4
+ # Addresses a single recording window by its (session_id, window_id) pair.
5
+ WindowRef = Data.define(:session_id, :window_id)
6
+ end
data/lib/sentiero.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sentiero/version"
4
+ require_relative "sentiero/redaction"
5
+ require_relative "sentiero/configuration"
6
+ require_relative "sentiero/ip_anonymizer"
7
+ require_relative "sentiero/store"
8
+ require_relative "sentiero/erasure"
9
+ require_relative "sentiero/stores/memory"
10
+ require_relative "sentiero/web/events_app"
11
+ require_relative "sentiero/web/ingest_app"
12
+ require_relative "sentiero/web/errors_app"
13
+ require_relative "sentiero/web/track_app"
14
+ require_relative "sentiero/fingerprint"
15
+ require_relative "sentiero/web/assets_app"
16
+ require_relative "sentiero/web/manifest"
17
+ require_relative "sentiero/web/base_app"
18
+ require_relative "sentiero/web/basic_auth"
19
+ require_relative "sentiero/web/dashboard_app"
20
+ require_relative "sentiero/web/analytics_app"
21
+ require_relative "sentiero/web/monitoring_app"
22
+ require_relative "sentiero/web/script_tag"
23
+ require_relative "sentiero/analytics/analyzer"
24
+
25
+ module Sentiero
26
+ class Error < StandardError; end
27
+
28
+ class << self
29
+ def configuration
30
+ @configuration ||= Configuration.new
31
+ end
32
+
33
+ def configure
34
+ yield(configuration)
35
+ end
36
+
37
+ def reset_configuration!
38
+ @configuration = Configuration.new
39
+ end
40
+
41
+ # Resets the core config plus the Rails and Reporter configs when those
42
+ # subsystems are loaded — one teardown for tests instead of three.
43
+ def reset_all_configuration!
44
+ reset_configuration!
45
+ Sentiero::Rails.reset_configuration! if defined?(Sentiero::Rails) && Sentiero::Rails.respond_to?(:reset_configuration!)
46
+ Sentiero::Reporter.reset! if defined?(Sentiero::Reporter) && Sentiero::Reporter.respond_to?(:reset!)
47
+ configuration
48
+ end
49
+
50
+ def store
51
+ configuration.store || raise(Error, "No store configured.")
52
+ end
53
+
54
+ def purge_expired!
55
+ period = configuration.retention_period
56
+ return unless period
57
+
58
+ store.purge_older_than(period)
59
+ end
60
+
61
+ def erase_sessions(ids)
62
+ Erasure.erase_sessions(store, ids)
63
+ end
64
+
65
+ def erase_where(since: nil, until_time: nil)
66
+ Erasure.erase_where(store, since: since, until_time: until_time)
67
+ end
68
+ end
69
+ end