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
data/README.md
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.svg" alt="Sentiero" width="80" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Sentiero</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>In-app browser session recording and replay for Ruby.</strong><br>
|
|
9
|
+
Self-hosted. Privacy-first. Framework-agnostic.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<hr/>
|
|
13
|
+
|
|
14
|
+
**Browser session recording for Ruby. Like Hotjar etc.**
|
|
15
|
+
|
|
16
|
+
Playback user journeys through your app to help debug issues, improve UX and understand intent.
|
|
17
|
+
|
|
18
|
+
Captures user interactions via [rrweb](https://www.rrweb.io/), stores them server-side with pluggable storage, and replays sessions from a built-in dashboard.
|
|
19
|
+
|
|
20
|
+
Similar to [SpectatorSport](https://github.com/bensheldon/spectator_sport) by Ben Sheldon but not bound to Rails.
|
|
21
|
+
|
|
22
|
+
📖 **Full documentation: [sentiero.app](https://sentiero.app)**
|
|
23
|
+
|
|
24
|
+
### Why Sentiero?
|
|
25
|
+
|
|
26
|
+
- **De-SaaS your session recording** — keep user interaction data in your own infrastructure instead of sending it to third-party services
|
|
27
|
+
- **Privacy-respecting defaults** — all inputs masked by default, password masking enforced and cannot be disabled, per-element control via HTML attributes
|
|
28
|
+
- **User-side controls** — respects Do Not Track (DNT) and Global Privacy Control (GPC), with support for explicit user opt-in/opt-out
|
|
29
|
+
- **Framework-agnostic** — drop into any Rack-compatible app, or use the dedicated Rails integration
|
|
30
|
+
- **Complete but focused** — session recording, replay, and the tools around them, without trying to be an analytics platform
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
Privacy-first defaults, no framework coupling, works with any Rack-based app.
|
|
35
|
+
|
|
36
|
+
- Replay dashboard with interactive event timeline, activity sidebar, and keyboard shortcuts
|
|
37
|
+
- Full DOM recording via [rrweb](https://www.rrweb.io/) with cross-tab session linking
|
|
38
|
+
- Privacy-first, inputs masked by default, password masking enforced
|
|
39
|
+
- Works with any Rack app (Rails, Roda, Sinatra)
|
|
40
|
+
|
|
41
|
+
Also:
|
|
42
|
+
|
|
43
|
+
- Session metadata, captures URL, browser, viewport, referrer (opt-in)
|
|
44
|
+
- Navigation tracking, automatic outbound link logging (opt-in)
|
|
45
|
+
- Error capture, JS errors recorded in the timeline (opt-in)
|
|
46
|
+
- Custom events, imperative JS API or declarative `data-sentiero-track-*` HTML attributes ([docs](https://sentiero.app/guide/custom-events/))
|
|
47
|
+
- JSON export and shareable deep-links with timestamp
|
|
48
|
+
- Replay enhancers, click overlay, scroll-depth indicator, frustration annotations (rage/dead clicks), form-interaction detail in the activity sidebar, Web Vitals badges, and a has-errors session filter
|
|
49
|
+
- Cross-session [Analytics](#analytics), pages, segments, errors, heatmaps, scroll, and forms across all recorded sessions
|
|
50
|
+
- [Shareable replays](#shareable-replays), export a self-contained HTML replay or play one back from JSON (opt-in)
|
|
51
|
+
- Privacy/compliance suite, end-user opt-out, GPC respect, server-side sanitization, IP anonymization, retention/purge, and right-to-erasure ([see below](#compliance))
|
|
52
|
+
- Pluggable storage, Memory, File, SQLite, Redis, or bring your own
|
|
53
|
+
- Gzip compression, smart batching, `sendBeacon` on page close, retry with backoff
|
|
54
|
+
|
|
55
|
+
> Run the [demo app](#demo-app) to see it in action: `demo/run` then visit `localhost:9292`.
|
|
56
|
+
|
|
57
|
+
| Demo app | Session list | Session replay |
|
|
58
|
+
|----------|-------------|----------------|
|
|
59
|
+
|  |  |  |
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
Add to your Gemfile:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
gem "sentiero"
|
|
67
|
+
|
|
68
|
+
# Optional, pick one for persistent storage:
|
|
69
|
+
gem "redis", ">= 4.0" # for Redis store
|
|
70
|
+
gem "sqlite3", ">= 1.4" # for SQLite store
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
### 1. Configure
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
require "sentiero"
|
|
79
|
+
|
|
80
|
+
Sentiero.configure do |config|
|
|
81
|
+
config.store = Sentiero::Stores::Memory.new
|
|
82
|
+
config.cors_origins = ["http://localhost:3000"]
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 2. Mount the Endpoints
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# Roda (via plugin)
|
|
90
|
+
require "sentiero/roda"
|
|
91
|
+
|
|
92
|
+
class MyApp < Roda
|
|
93
|
+
plugin :sentiero
|
|
94
|
+
|
|
95
|
+
route do |r|
|
|
96
|
+
r.on "sentiero" do
|
|
97
|
+
r.on("events") { r.sentiero_events }
|
|
98
|
+
r.sentiero_dashboard
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# Plain Rack (config.ru)
|
|
106
|
+
map("/sentiero/events") { run Sentiero::Web::EventsApp.new }
|
|
107
|
+
map("/sentiero") { run Sentiero::Web::DashboardApp.new }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# Rails (routes.rb)
|
|
112
|
+
mount Sentiero::Web::EventsApp.new => "/sentiero/events"
|
|
113
|
+
mount Sentiero::Web::DashboardApp.new => "/sentiero"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Two mounts are all you need: `DashboardApp` is the single dashboard entry
|
|
117
|
+
point and internally dispatches `/analytics/*`, `/issues/*`, `/custom-events/*`,
|
|
118
|
+
`/assets/*`, and `/recorder.js`. Don't mount `AnalyticsApp` or `MonitoringApp`
|
|
119
|
+
at their own paths alongside it — that creates routing conflicts.
|
|
120
|
+
|
|
121
|
+
> **Behind a reverse proxy?** Mount Sentiero at the same path the proxy
|
|
122
|
+
> forwards, and don't strip the prefix: the dashboard builds its links from
|
|
123
|
+
> Rack's `SCRIPT_NAME`, which `map`/`mount` set. With a prefix-stripping proxy
|
|
124
|
+
> (Caddy's `handle_path`, nginx `proxy_pass` with a trailing slash),
|
|
125
|
+
> `SCRIPT_NAME` stays empty and every internal link points at the root. Use
|
|
126
|
+
> Caddy's `handle` (not `handle_path`) / nginx `proxy_pass` without a URI, and
|
|
127
|
+
> keep the `map("/sentiero")` in your config.ru.
|
|
128
|
+
|
|
129
|
+
### 3. Add the Recording Script
|
|
130
|
+
|
|
131
|
+
Include in your HTML layout (before `</body>`):
|
|
132
|
+
|
|
133
|
+
```erb
|
|
134
|
+
<%= Sentiero::Web::ScriptTag.render(events_url: "/sentiero/events") %>
|
|
135
|
+
|
|
136
|
+
<%# Or with the Roda plugin helper: %>
|
|
137
|
+
<%= sentiero_script_tag(events_url: "/sentiero/events") %>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
That's it. Sessions are now being recorded and viewable at `/sentiero/`.
|
|
141
|
+
|
|
142
|
+
> **Using Rails?** The `sentiero-rails` gem adds ActiveRecord storage, a migration generator, and view helpers. See [the Rails guide](https://sentiero.app/guide/rails/) for the full guide.
|
|
143
|
+
|
|
144
|
+
> **Going to production?** Read the [Production Checklist](#production-checklist) first.
|
|
145
|
+
|
|
146
|
+
## Authentication
|
|
147
|
+
|
|
148
|
+
**The dashboard has no authentication by default.** Anyone who can reach the URL can view and delete recorded sessions.
|
|
149
|
+
|
|
150
|
+
There are two approaches to protect it:
|
|
151
|
+
|
|
152
|
+
**1. `auth_callback`, for session-based auth** (Devise, Warden, custom sessions):
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
Sentiero.configure do |config|
|
|
156
|
+
config.auth_callback = ->(env) { env["warden"]&.user&.admin? }
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Denied requests get a `403 Forbidden`. This works when the user already has a session cookie, it won't trigger a browser login dialog.
|
|
161
|
+
|
|
162
|
+
**2. Route-level auth, for challenge-based auth** (HTTP Basic, OAuth redirects):
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
# Roda example (from the demo app)
|
|
166
|
+
r.on "sentiero" do
|
|
167
|
+
r.on("events") { r.sentiero_events }
|
|
168
|
+
|
|
169
|
+
auth = Rack::Auth::Basic::Request.new(r.env)
|
|
170
|
+
unless auth.provided? && auth.basic? && auth.credentials == [user, password]
|
|
171
|
+
r.halt [401, {"www-authenticate" => 'Basic realm="Dashboard"'}, ["Unauthorized"]]
|
|
172
|
+
end
|
|
173
|
+
r.sentiero_dashboard
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Use route-level auth when you need `401`/`302` responses (e.g., HTTP Basic prompts, OAuth redirects). `auth_callback` only returns `403`.
|
|
178
|
+
|
|
179
|
+
The events endpoint (`EventsApp`) is intentionally public, it receives browser-generated rrweb data. Protect it with CORS (`cors_origins`), rate limiting, and payload size limits instead.
|
|
180
|
+
|
|
181
|
+
See [the authentication guide](https://sentiero.app/guide/authentication/) for the full guide including Rails, Sinatra, and plain Rack examples.
|
|
182
|
+
|
|
183
|
+
## Privacy
|
|
184
|
+
|
|
185
|
+
All DOM inputs are masked by default, values entered into form fields are not sent to the backend.
|
|
186
|
+
|
|
187
|
+
Password masking is **enforced and cannot be disabled**.
|
|
188
|
+
|
|
189
|
+
> **Input masking ≠ all PII.** Masking covers values the user *types*. PII the
|
|
190
|
+
> server *renders into the page* — `Welcome, Ada Lovelace!`, an order summary, an
|
|
191
|
+
> account number — is captured as ordinary DOM text. Mark those elements with
|
|
192
|
+
> `data-rr-mask` (mask the text) or `data-rr-block` (drop the element entirely):
|
|
193
|
+
>
|
|
194
|
+
> ```erb
|
|
195
|
+
> <h1 data-rr-mask>Welcome, <%= current_user.name %>!</h1>
|
|
196
|
+
> ```
|
|
197
|
+
>
|
|
198
|
+
> As a server-side backstop for text you can pattern-match but can't annotate,
|
|
199
|
+
> add a pattern to `config.redaction.custom_patterns` or a
|
|
200
|
+
> `config.redaction.server_proc` hook (see [Compliance](#compliance)).
|
|
201
|
+
|
|
202
|
+
You can control which inputs are masked and recorded. You can also block whole sections of content, useful for areas that may contain PII or sensitive information (user-generated content, one-time token displays, etc.).
|
|
203
|
+
|
|
204
|
+
### Per-Element Control
|
|
205
|
+
|
|
206
|
+
Use HTML attributes to control recording on individual elements:
|
|
207
|
+
|
|
208
|
+
```html
|
|
209
|
+
<div data-rr-block>Blocked from recording entirely</div>
|
|
210
|
+
<span data-rr-mask>Text content masked in replay</span>
|
|
211
|
+
<input data-rr-ignore> <!-- Mutations not recorded -->
|
|
212
|
+
<input type="password"> <!-- Always masked, enforced -->
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Selective Unmasking
|
|
216
|
+
|
|
217
|
+
When global masking is on (the default), you can selectively unmask specific inputs or sections with `data-sentiero-unmask`:
|
|
218
|
+
|
|
219
|
+
```html
|
|
220
|
+
<!-- Unmask a single input -->
|
|
221
|
+
<input type="text" name="search" data-sentiero-unmask>
|
|
222
|
+
|
|
223
|
+
<!-- Unmask an entire section (applies to all inputs and text within) -->
|
|
224
|
+
<div data-sentiero-unmask>
|
|
225
|
+
<input type="text" name="first_name">
|
|
226
|
+
<input type="text" name="last_name">
|
|
227
|
+
<span>This text content is also unmasked</span>
|
|
228
|
+
</div>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
In Rails ERB:
|
|
232
|
+
|
|
233
|
+
```erb
|
|
234
|
+
<%= f.text_field :search, data: { sentiero_unmask: true } %>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Important:** Password inputs remain masked even with `data-sentiero-unmask`. Password masking is enforced by rrweb independently and cannot be bypassed.
|
|
238
|
+
|
|
239
|
+
**Known limitation:** Some rrweb versions don't consistently call masking functions during the initial full DOM snapshot. Pre-filled input values present on page load may appear masked in the snapshot even if marked with `data-sentiero-unmask`. Values captured from input events after page load work correctly.
|
|
240
|
+
|
|
241
|
+
### Compliance
|
|
242
|
+
|
|
243
|
+
Sentiero ships a compliance toolkit for GDPR/CCPA-style obligations:
|
|
244
|
+
|
|
245
|
+
- **End-user opt-out** — set `config.user_opt_out = true` to expose `window.Sentiero.optOut()` / `optIn()` in the browser. Opting out drops a cookie (`config.opt_out_cookie_name`, default `"sentiero_optout"`) and stops recording across sessions; the server respects the same cookie.
|
|
246
|
+
- **Global Privacy Control** — `config.respect_gpc` (default `true`) suppresses recording for visitors sending the GPC signal.
|
|
247
|
+
- **Server-side redaction** — the `config.redaction` engine scrubs events on ingest before they reach the store: builtin patterns (emails, tokens, cards), URL query handling (`url_mode`, allow/denylists), `custom_patterns` for server-rendered PII you can pattern-match, and a `server_proc` hook (the ingest-side backstop to `data-rr-mask`/`data-rr-block`). Redaction is fail-closed: an error in `server_proc` drops the batch rather than persisting unsanitized data.
|
|
248
|
+
- **IP anonymization** — `config.anonymize_ip` (default `true`) truncates client IPs before storage; set to `false` to keep raw IPs.
|
|
249
|
+
- **Data retention / purge** — set `config.retention_period` (seconds) and call `Sentiero.purge_expired!` from a scheduler, or run `rake sentiero:purge` in Rails apps.
|
|
250
|
+
- **Right to erasure** — `Sentiero.erase_sessions(*ids)` / `Sentiero.erase_where(**filters)`, or `rake sentiero:erase` in Rails apps.
|
|
251
|
+
- **Audit hook** — `config.audit_log` receives compliance-relevant events (opt-outs, erasures, purges) for your own logging.
|
|
252
|
+
|
|
253
|
+
See [the privacy guide](https://sentiero.app/guide/privacy/) for the full privacy guide including cross-tab sessions, global recording options, and compliance details.
|
|
254
|
+
|
|
255
|
+
## Analytics
|
|
256
|
+
|
|
257
|
+
Beyond single-session replay, Sentiero includes a cross-session analytics dashboard that aggregates behavior across all recorded sessions. It's served under the dashboard mount at `/analytics`:
|
|
258
|
+
|
|
259
|
+
| Path | View |
|
|
260
|
+
|------|------|
|
|
261
|
+
| `/analytics` | Pages overview |
|
|
262
|
+
| `/analytics/segments` | Segments (browser, viewport, referrer, etc.) |
|
|
263
|
+
| `/analytics/errors` | Captured JS errors |
|
|
264
|
+
| `/analytics/heatmap` | Click heatmap |
|
|
265
|
+
| `/analytics/scroll` | Scroll-depth |
|
|
266
|
+
| `/analytics/forms` | Form interactions |
|
|
267
|
+
| `/analytics/export` | Export aggregated data |
|
|
268
|
+
|
|
269
|
+
Analytics are **compute-on-read**: there are no rollup tables, the analyzers query the store and aggregate at request time. To keep that bounded, each request scans at most `config.analytics_max_scan_sessions` sessions (default `5000`).
|
|
270
|
+
|
|
271
|
+
DashboardApp serves these routes automatically. To mount the analytics UI on its own, use the Roda helper `r.sentiero_analytics`.
|
|
272
|
+
|
|
273
|
+
See [the analytics guide](https://sentiero.app/guide/analytics/) for details.
|
|
274
|
+
|
|
275
|
+
## Shareable Replays
|
|
276
|
+
|
|
277
|
+
You can hand a single session to someone who has no access to your dashboard:
|
|
278
|
+
|
|
279
|
+
- **HTML export** — `GET /analytics/share/:id` produces a standalone, self-contained HTML file that replays the session with no server needed.
|
|
280
|
+
- **JSON import** — `/analytics/import` plays a session back from a previously exported JSON dump.
|
|
281
|
+
|
|
282
|
+
Both are gated by `config.shareable_replays` (default `false`); while disabled, the routes return `404`.
|
|
283
|
+
|
|
284
|
+
> **Security:** a share file is a full session dump that leaves your infrastructure. Treat it like any other export of recorded data and only enable sharing if that's acceptable for your privacy posture.
|
|
285
|
+
|
|
286
|
+
See [the sharing guide](https://sentiero.app/guide/sharing/) for details.
|
|
287
|
+
|
|
288
|
+
## Server-side Error Tracking
|
|
289
|
+
|
|
290
|
+
Sentiero ships a server-side **reporter** that sends unhandled exceptions and
|
|
291
|
+
custom events to a Sentiero ingest endpoint, where they are fingerprinted into
|
|
292
|
+
issues. When a server error happens during a recorded session, the reporter
|
|
293
|
+
links it back to the front-end replay via the `sentiero_sid` / `sentiero_wid`
|
|
294
|
+
cookies, so you can watch what the user did right before it broke.
|
|
295
|
+
|
|
296
|
+
The reporter is **fail-safe**: every public method rescues internally and never
|
|
297
|
+
raises into your app.
|
|
298
|
+
|
|
299
|
+
### Install
|
|
300
|
+
|
|
301
|
+
The reporter lives in the same gem, but isn't required by default. Require it
|
|
302
|
+
where you configure it (the Rails initializer does this for you):
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
require "sentiero/reporter"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Configure
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
Sentiero::Reporter.configure do |r|
|
|
312
|
+
r.endpoint = ENV["SENTIERO_ENDPOINT"] # e.g. "https://sentiero.example.com"
|
|
313
|
+
r.ingest_key = ENV["SENTIERO_INGEST_KEY"] # server-issued ingest key
|
|
314
|
+
r.project = "my-app" # project identifier
|
|
315
|
+
r.environment = "production" # added to every report's context
|
|
316
|
+
r.release = ENV["GIT_SHA"] # optional release/version
|
|
317
|
+
|
|
318
|
+
r.ignore_exceptions = [ActiveRecord::RecordNotFound, "ActionController::RoutingError"]
|
|
319
|
+
r.before_notify = ->(report) {
|
|
320
|
+
report["context"].delete("secret")
|
|
321
|
+
report # return false/nil to drop the report
|
|
322
|
+
}
|
|
323
|
+
r.filter_keys = [:password, :token, /secret/i]
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The reporter is **active** only when `endpoint`, `ingest_key`, and `project` are
|
|
328
|
+
all set and `enabled` is true (the default). Until then, `notify`/`track` are
|
|
329
|
+
no-ops.
|
|
330
|
+
|
|
331
|
+
### Report errors and events
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
# Report a caught exception
|
|
335
|
+
begin
|
|
336
|
+
risky!
|
|
337
|
+
rescue => e
|
|
338
|
+
Sentiero::Reporter.notify(e, context: { user_id: current_user.id })
|
|
339
|
+
raise
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Track a custom event (a non-error business signal)
|
|
343
|
+
Sentiero::Reporter.track("signup", level: "info", plan: "pro")
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Context
|
|
347
|
+
|
|
348
|
+
Attach context once and have it merged into every report from the current
|
|
349
|
+
thread:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
Sentiero::Reporter.add_context(user_id: 42) # sticky for this thread
|
|
353
|
+
|
|
354
|
+
Sentiero::Reporter.with_context(request_id: "abc") do # scoped to the block
|
|
355
|
+
Sentiero::Reporter.notify(error)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
Sentiero::Reporter.clear_context
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Putting `session_id` / `window_id` in context (the Rack middleware does this
|
|
362
|
+
from the recorder cookies) links the report to a session replay.
|
|
363
|
+
|
|
364
|
+
### Filtering noise
|
|
365
|
+
|
|
366
|
+
- `ignore_exceptions` — an array of exception classes or `"String"` class-names;
|
|
367
|
+
matches the exception class **or any ancestor**, so a subclass of an ignored
|
|
368
|
+
error is also dropped.
|
|
369
|
+
- `before_notify` — a proc called with the assembled, mutable report hash. Mutate
|
|
370
|
+
it in place to enrich/redact, or return `false`/`nil` to drop the report. A
|
|
371
|
+
raising hook is caught and the original report is delivered unchanged.
|
|
372
|
+
|
|
373
|
+
### Transports
|
|
374
|
+
|
|
375
|
+
By default the reporter posts JSON to your endpoint. For development/test you
|
|
376
|
+
can swap the transport:
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
r.transport = Sentiero::Reporter::LogTransport.new # logs would-be deliveries
|
|
380
|
+
r.transport = Sentiero::Reporter::NullTransport.new # drops everything
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
In your own test suite, capture what would have been sent. The helper is not
|
|
384
|
+
loaded by default — require it from your test setup:
|
|
385
|
+
|
|
386
|
+
```ruby
|
|
387
|
+
require "sentiero/reporter/test_helper"
|
|
388
|
+
|
|
389
|
+
captured = Sentiero::Reporter::TestHelper.capture_notifications do
|
|
390
|
+
do_something_that_reports
|
|
391
|
+
end
|
|
392
|
+
# => [["errors", {...}], ["track", {...}]]
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Rails auto-install
|
|
396
|
+
|
|
397
|
+
With `sentiero-rails`, configuring the reporter is all you need — the engine
|
|
398
|
+
auto-inserts `Sentiero::Reporter::Middleware` into your middleware stack (it
|
|
399
|
+
captures and re-raises unhandled exceptions, and reads the session/window
|
|
400
|
+
cookies into context). Opt out with `Sentiero::Rails.configure { |c| c.reporter_middleware = false }`.
|
|
401
|
+
For non-Rails apps, add the middleware yourself: `use Sentiero::Reporter::Middleware`.
|
|
402
|
+
|
|
403
|
+
See [the error tracking guide](https://sentiero.app/guide/error-tracking/) for the full guide
|
|
404
|
+
(architecture, the `/issues` and `/custom-events` dashboards, deployment, and
|
|
405
|
+
the Crystal shard).
|
|
406
|
+
|
|
407
|
+
## Configuration
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
Sentiero.configure do |config|
|
|
411
|
+
# Storage backend (required)
|
|
412
|
+
config.store = Sentiero::Stores::Redis.new(
|
|
413
|
+
redis: Redis.new(url: ENV["REDIS_URL"]),
|
|
414
|
+
ttl: 86_400 * 7, # auto-expire after 7 days
|
|
415
|
+
prefix: "sentiero:" # Redis key namespace
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# CORS origins for the events endpoint
|
|
419
|
+
config.cors_origins = ["https://mysite.com"]
|
|
420
|
+
|
|
421
|
+
# Dashboard auth (receives Rack env, return truthy to allow)
|
|
422
|
+
config.auth_callback = ->(env) { env["rack.session"]&.dig("admin") }
|
|
423
|
+
|
|
424
|
+
# Frontend flush tuning
|
|
425
|
+
config.flush_interval_ms = 10_000 # time-based flush (ms), default: 10,000
|
|
426
|
+
config.flush_event_threshold = 50 # count-based flush, default: 50
|
|
427
|
+
config.max_events_per_page = 1_000 # events API pagination, default: 1,000
|
|
428
|
+
|
|
429
|
+
# Resource limits (nil = unlimited)
|
|
430
|
+
config.max_events_per_request = 500 # max events in a single POST
|
|
431
|
+
config.max_sessions = 10_000 # max sessions in store (LRU eviction)
|
|
432
|
+
config.max_events_per_session = 50_000 # max events per session (oldest dropped)
|
|
433
|
+
|
|
434
|
+
# Cross-tab session linking (default: true)
|
|
435
|
+
# When true, all tabs share a session ID (via localStorage).
|
|
436
|
+
# When false, each tab gets its own session (via sessionStorage).
|
|
437
|
+
config.cross_tab_sessions = true
|
|
438
|
+
|
|
439
|
+
# Opt-in features (all default: false)
|
|
440
|
+
config.capture_metadata = true # capture URL, browser, viewport, referrer
|
|
441
|
+
config.capture_errors = true # capture JS errors as timeline events
|
|
442
|
+
config.track_navigation = true # log outbound link clicks as events
|
|
443
|
+
config.track_custom_events = true # enable declarative data-sentiero-track-* attributes
|
|
444
|
+
config.track_forms = true # capture real form submits for form analytics (attrs only, never values)
|
|
445
|
+
|
|
446
|
+
# rrweb recorder options (Ruby-style snake_case)
|
|
447
|
+
config.mask_all_inputs = true # default: true
|
|
448
|
+
config.block_selector = "[data-rr-block]" # default: "[data-rr-block]"
|
|
449
|
+
config.mask_text_selector = "[data-rr-mask]" # default: "[data-rr-mask]"
|
|
450
|
+
config.ignore_selector = "[data-rr-ignore]" # default: "[data-rr-ignore]"
|
|
451
|
+
config.sampling = { scroll: 150, input: "last" } # default shown
|
|
452
|
+
config.inline_stylesheet = true # default: nil (rrweb default)
|
|
453
|
+
config.checkout_every_n_ms = 30_000 # default: nil (rrweb default)
|
|
454
|
+
|
|
455
|
+
# Escape hatch for rrweb options without a first-class attribute.
|
|
456
|
+
# Pass camelCase keys; they're forwarded to rrweb verbatim.
|
|
457
|
+
# First-class attributes above take precedence for overlapping keys.
|
|
458
|
+
config.recorder_options = { someObscureOption: true }
|
|
459
|
+
end
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
| Option | Type | Default | Description |
|
|
463
|
+
|--------|------|---------|-------------|
|
|
464
|
+
| `store` | `Sentiero::Store` | `nil` (required) | Storage backend instance |
|
|
465
|
+
| `cors_origins` | `Array<String>` | `[]` | Allowed CORS origins for EventsApp |
|
|
466
|
+
| `auth_callback` | `Proc` / `nil` | `nil` | Dashboard auth; `nil` = open access |
|
|
467
|
+
| `flush_interval_ms` | `Integer` | `10_000` | Frontend time-based flush interval |
|
|
468
|
+
| `flush_event_threshold` | `Integer` | `50` | Frontend count-based flush threshold |
|
|
469
|
+
| `max_events_per_page` | `Integer` | `1_000` | Max events returned per API page |
|
|
470
|
+
| `max_events_per_request` | `Integer` / `nil` | `nil` | Max events accepted per POST; `nil` = unlimited |
|
|
471
|
+
| `max_sessions` | `Integer` / `nil` | `nil` | Max sessions in store; oldest evicted when exceeded; `nil` = unlimited |
|
|
472
|
+
| `max_events_per_session` | `Integer` / `nil` | `nil` | Max events per session; oldest dropped when exceeded; `nil` = unlimited |
|
|
473
|
+
| `cross_tab_sessions` | `Boolean` | `true` | Link sessions across browser tabs; `false` isolates each tab as its own session |
|
|
474
|
+
| `session_idle_timeout` | `Integer` | `21_600` (6 h) | Inactivity gap (seconds) after which a returning visitor starts a new session |
|
|
475
|
+
| `session_max_age` | `Integer` | `604_800` (7 d) | Hard cap (seconds) on a session ID's lifetime, even while continuously active |
|
|
476
|
+
| `capture_metadata` | `Boolean` | `false` | Capture page URL, browser, viewport, and referrer per session |
|
|
477
|
+
| `capture_errors` | `Boolean` | `false` | Capture JS errors and unhandled promise rejections as timeline events |
|
|
478
|
+
| `track_navigation` | `Boolean` | `false` | Automatically log outbound link clicks as custom events |
|
|
479
|
+
| `track_custom_events` | `Boolean` | `false` | Enable declarative `data-sentiero-track-*` HTML attributes for custom events ([docs](https://sentiero.app/guide/custom-events/)) |
|
|
480
|
+
| `track_forms` | `Boolean` | `false` | Capture real form submits as `__form_submit` events for form analytics (form `name`/`id` attributes + page URL — never values) |
|
|
481
|
+
| `mask_all_inputs` | `Boolean` | `true` | Mask all form input values in recordings |
|
|
482
|
+
| `mask_input_options` | `Hash` | `{}` | Per-input-type masking; `password: true` is always enforced |
|
|
483
|
+
| `block_selector` | `String` / `nil` | `"[data-rr-block]"` | CSS selector for elements excluded from recording |
|
|
484
|
+
| `mask_text_selector` | `String` / `nil` | `"[data-rr-mask]"` | CSS selector for elements with masked text content |
|
|
485
|
+
| `ignore_selector` | `String` / `nil` | `"[data-rr-ignore]"` | CSS selector for elements whose mutations are ignored |
|
|
486
|
+
| `sampling` | `Hash` / `nil` | `{ scroll: 150, input: "last" }` | Throttling for scroll/input events |
|
|
487
|
+
| `inline_stylesheet` | `Boolean` / `nil` | `nil` | Inline stylesheets into the recording snapshot |
|
|
488
|
+
| `checkout_every_n_ms` | `Integer` / `nil` | `nil` | Full DOM checkout interval in milliseconds |
|
|
489
|
+
| `recorder_options` | `Hash` | `{}` | Escape hatch: raw camelCase rrweb options forwarded verbatim; first-class attributes above take precedence for overlapping keys |
|
|
490
|
+
| `capture_web_vitals` | `Boolean` | `false` | Capture Web Vitals (LCP, CLS, etc.) and show them as badges in replay |
|
|
491
|
+
| `analytics_max_scan_sessions` | `Integer` | `5000` | Max sessions scanned per compute-on-read analytics request |
|
|
492
|
+
| `shareable_replays` | `Boolean` | `false` | Enable HTML-export and JSON-import share routes; `false` makes them `404` |
|
|
493
|
+
| `user_opt_out` | `Boolean` | `false` | Expose `window.Sentiero.optOut()` / `optIn()` and honor the opt-out cookie |
|
|
494
|
+
| `opt_out_cookie_name` | `String` | `"sentiero_optout"` | Cookie name used to persist an end-user opt-out |
|
|
495
|
+
| `respect_gpc` | `Boolean` | `true` | Suppress recording for visitors sending a Global Privacy Control signal |
|
|
496
|
+
| `retention_period` | `Integer` / `nil` | `nil` | Retention window in seconds for `purge_expired!`; `nil` = keep forever |
|
|
497
|
+
| `redaction` | `Redaction::Config` | builtin defaults | Client+server redaction engine (`url_mode`, `custom_patterns`, `server_proc`); fail-closed |
|
|
498
|
+
| `anonymize_ip` | `Boolean` | `true` | Truncate client IP addresses before storage |
|
|
499
|
+
| `audit_log` | `Proc` / `nil` | `nil` | Hook receiving compliance events (opt-outs, erasures, purges) |
|
|
500
|
+
|
|
501
|
+
### Reporter configuration (`Sentiero::Reporter.configure`)
|
|
502
|
+
|
|
503
|
+
These keys live on `Sentiero::Reporter::Configuration`, not the recorder config above.
|
|
504
|
+
|
|
505
|
+
| Option | Type | Default | Description |
|
|
506
|
+
|--------|------|---------|-------------|
|
|
507
|
+
| `endpoint` | `String` / `nil` | `nil` | Ingest base URL; required for the reporter to be active |
|
|
508
|
+
| `ingest_key` | `String` / `nil` | `nil` | Bearer ingest key; required for the reporter to be active |
|
|
509
|
+
| `project` | `String` / `nil` | `nil` | Project identifier; required for the reporter to be active |
|
|
510
|
+
| `environment` | `String` / `nil` | `nil` | Added to every report's context |
|
|
511
|
+
| `release` | `String` / `nil` | `nil` | Release/version added to every report's context |
|
|
512
|
+
| `enabled` | `Boolean` | `true` | Master switch; `false` makes `notify`/`track` no-ops |
|
|
513
|
+
| `ignore_exceptions` | `Array<Class, String>` | `[]` | Exceptions to drop, matched by class/ancestor or class-name string |
|
|
514
|
+
| `before_notify` | `Proc` / `nil` | `nil` | Hook to mutate the report hash or drop it (return `false`/`nil`) |
|
|
515
|
+
| `filter_keys` | `Array<String, Symbol, Regexp>` | `[]` | Context/payload keys to redact before sending |
|
|
516
|
+
| `async` | `Boolean` | `true` | Deliver via background queue; `false` delivers synchronously |
|
|
517
|
+
| `max_queue` | `Integer` | `100` | Max queued payloads before new ones are dropped (async) |
|
|
518
|
+
| `transport` | object / `nil` | `nil` | Custom transport (`post(path, payload)`); `nil` = `HttpTransport` |
|
|
519
|
+
| `open_timeout` | `Integer` | `2` | HTTP open timeout (seconds) |
|
|
520
|
+
| `read_timeout` | `Integer` | `3` | HTTP read timeout (seconds) |
|
|
521
|
+
| `session_cookie_name` | `String` | `"sentiero_sid"` | Cookie read for session-replay linkage |
|
|
522
|
+
| `window_cookie_name` | `String` | `"sentiero_wid"` | Cookie read for session-replay linkage |
|
|
523
|
+
|
|
524
|
+
Rails-only: `Sentiero::Rails.configure { |c| c.reporter_middleware = ... }` (default `true`) controls the middleware auto-install.
|
|
525
|
+
|
|
526
|
+
## Production Checklist
|
|
527
|
+
|
|
528
|
+
Before deploying Sentiero to production, review each of these:
|
|
529
|
+
|
|
530
|
+
| Concern | Action |
|
|
531
|
+
|---------|--------|
|
|
532
|
+
| **HTTPS** | Always serve over HTTPS in production |
|
|
533
|
+
| **Dashboard auth** | Set `auth_callback`, without it, the dashboard is open to anyone |
|
|
534
|
+
| **Privacy compliance** | Review recording options for your jurisdiction; add `data-rr-block` to sensitive sections |
|
|
535
|
+
| **Encryption at rest** | Enable at the storage layer if recording sensitive pages |
|
|
536
|
+
| **Resource limits** | Set `max_events_per_request`, `max_sessions`, and `max_events_per_session` to prevent unbounded memory growth |
|
|
537
|
+
| **Rate limiting** | Add [Rack::Attack](https://github.com/rack/rack-attack) or nginx rate limiting on the events endpoint |
|
|
538
|
+
| **Store** | Use Redis with TTL or SQLite in production; Memory and File stores are for dev/small deployments |
|
|
539
|
+
| **CORS** | Set `cors_origins` to your frontend's origin(s) |
|
|
540
|
+
|
|
541
|
+
## Storage Backends
|
|
542
|
+
|
|
543
|
+
### Memory (dev/test)
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
config.store = Sentiero::Stores::Memory.new
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
In-memory storage using `concurrent-ruby` thread-safe primitives. Data is lost on restart.
|
|
550
|
+
|
|
551
|
+
### File (dev/small deployments)
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
require "sentiero/stores/file"
|
|
555
|
+
|
|
556
|
+
config.store = Sentiero::Stores::File.new(path: "tmp/sentiero_sessions")
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Stores sessions as JSON files on disk — one directory per session, one `.jsonl` file per window. Persists across restarts with zero dependencies. Not suitable for high-concurrency production use.
|
|
560
|
+
|
|
561
|
+
| Parameter | Type | Default | Description |
|
|
562
|
+
|-----------|------|---------|-------------|
|
|
563
|
+
| `path` | `String` | (required) | Directory path for session data |
|
|
564
|
+
|
|
565
|
+
### SQLite (single-server production)
|
|
566
|
+
|
|
567
|
+
```ruby
|
|
568
|
+
require "sentiero/stores/sqlite"
|
|
569
|
+
|
|
570
|
+
config.store = Sentiero::Stores::SQLite.new(path: "sentiero.db")
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Single-file database with WAL mode. Good for single-process production and small deployments without external services. Requires the `sqlite3` gem.
|
|
574
|
+
|
|
575
|
+
| Parameter | Type | Default | Description |
|
|
576
|
+
|-----------|------|---------|-------------|
|
|
577
|
+
| `path` | `String` | `"sentiero.db"` | Database file path (use `":memory:"` for in-memory) |
|
|
578
|
+
|
|
579
|
+
### Redis (production)
|
|
580
|
+
|
|
581
|
+
```ruby
|
|
582
|
+
require "sentiero/stores/redis"
|
|
583
|
+
|
|
584
|
+
config.store = Sentiero::Stores::Redis.new(
|
|
585
|
+
redis: Redis.new(url: "redis://localhost:6379/0"),
|
|
586
|
+
ttl: 86_400, # optional, seconds
|
|
587
|
+
prefix: "sentiero:" # optional, default: "sentiero:"
|
|
588
|
+
)
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Requires the `redis` gem.
|
|
592
|
+
|
|
593
|
+
| Parameter | Type | Default | Description |
|
|
594
|
+
|-----------|------|---------|-------------|
|
|
595
|
+
| `redis` | `::Redis` | (required) | Connected Redis client |
|
|
596
|
+
| `ttl` | `Integer` / `nil` | `nil` | TTL in seconds applied to all keys; `nil` = no expiry |
|
|
597
|
+
| `prefix` | `String` | `"sentiero:"` | Key namespace prefix |
|
|
598
|
+
|
|
599
|
+
### Custom Store
|
|
600
|
+
|
|
601
|
+
Implement the methods defined in `Sentiero::Store` and verify with the shared contract tests.
|
|
602
|
+
|
|
603
|
+
See [the storage guide](https://sentiero.app/guide/storage/) for details on all backends, Redis data structures, and building custom stores.
|
|
604
|
+
|
|
605
|
+
## Security
|
|
606
|
+
|
|
607
|
+
Sentiero has undergone multiple review passes covering XSS, CSRF, Redis key poisoning, gzip bomb attacks, payload-based OOM vectors, and other common vulnerability classes.
|
|
608
|
+
|
|
609
|
+
However, this is a new release and has not yet been battle-tested or reviewed by a 3rd party expert.
|
|
610
|
+
|
|
611
|
+
I can't obviously guarantee the absence of undiscovered vulnerabilities.
|
|
612
|
+
|
|
613
|
+
If you discover a security issue, please report it privately via [GitHub Security Advisories](https://github.com/stevegeek/sentiero/security/advisories/new), do not open a public issue. See [SECURITY.md](SECURITY.md) for full details, including a list of areas already reviewed.
|
|
614
|
+
|
|
615
|
+
## Demo App
|
|
616
|
+
|
|
617
|
+
A Roda todo list app demonstrating full Sentiero integration with privacy features and dashboard auth.
|
|
618
|
+
|
|
619
|
+
```bash
|
|
620
|
+
demo/run # Install deps + start server on :9292
|
|
621
|
+
demo/run help # See all commands
|
|
622
|
+
demo/run stop # Stop the server
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
- **Todo app**: http://localhost:9292
|
|
626
|
+
- **Dashboard**: http://localhost:9292/sentiero/dashboard/ (credentials: `demo` / `demo`)
|
|
627
|
+
- Configurable via `demo/.env` (defaults) and `demo/.env.development` (local overrides)
|
|
628
|
+
- Set `REDIS_URL` in `.env.development` to use Redis; omit for File store (auto-fallback on connection failure, persists across restarts)
|
|
629
|
+
|
|
630
|
+
## Development
|
|
631
|
+
|
|
632
|
+
### Setup
|
|
633
|
+
|
|
634
|
+
```bash
|
|
635
|
+
bin/dev up # Start Redis via Docker
|
|
636
|
+
bin/dev down # Stop services
|
|
637
|
+
bin/dev console # Open redis-cli
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Frontend Build
|
|
641
|
+
|
|
642
|
+
The recorder JS is pre-built and vendored in the gem. You only need this if modifying the frontend source:
|
|
643
|
+
|
|
644
|
+
```bash
|
|
645
|
+
cd frontend && npm install && npm run build
|
|
646
|
+
# Output: lib/sentiero/web/assets/vendor/recorder.js
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
See [the recorder guide](https://sentiero.app/guide/recorder/) for details on the frontend recorder's batching, compression, and retry behavior.
|
|
650
|
+
|
|
651
|
+
### Testing
|
|
652
|
+
|
|
653
|
+
```bash
|
|
654
|
+
bin/dev test # Start Redis + run all tests
|
|
655
|
+
bundle exec rake test # All tests (Redis must be running)
|
|
656
|
+
bundle exec rake test TEST=test/stores/memory_test.rb # Single file
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
**Store contract tests** verify any store implementation against the required interface. Include `StoreContractTests` in your test class and implement `create_store`.
|
|
660
|
+
|
|
661
|
+
**Redis tests** skip automatically when Redis is unavailable.
|
|
662
|
+
|
|
663
|
+
## Alternatives
|
|
664
|
+
|
|
665
|
+
Sentiero was initially inspired by [SpectatorSport](https://github.com/bensheldon/spectator_sport) by Ben Sheldon.
|
|
666
|
+
|
|
667
|
+
It was the first `rrweb` session recording gem I saw for Ruby, but I needed something but not bound to Rails.
|
|
668
|
+
|
|
669
|
+
Go check out `spectator_sport` too.
|
|
670
|
+
|
|
671
|
+
## Thanks
|
|
672
|
+
|
|
673
|
+
Powered by [rrweb](https://www.rrweb.io/).
|
|
674
|
+
|
|
675
|
+
Coded by Claude Code and Stephen Ierodiaconou.
|
|
676
|
+
|
|
677
|
+
## License
|
|
678
|
+
|
|
679
|
+
[MIT](LICENSE.txt) - Copyright 2026 Stephen Ierodiaconou
|