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,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,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" : "—"
|
|
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"
|
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
|