Module: Sentiero::Store::SessionStore

Included in:
Sentiero::Store
Defined in:
lib/sentiero/store/session_store.rb

Overview

The session-replay store contract: recording windows of rrweb events and reading them back as sessions.

Window-level methods take a Sentiero::WindowRef; session-level methods take a bare session_id.

Instance Method Summary collapse

Instance Method Details

#delete_session(session_id) ⇒ Object

Raises:

  • (NoMethodError)


32
33
34
# File 'lib/sentiero/store/session_store.rb', line 32

def delete_session(session_id)
  raise NoMethodError, "#{self.class}#delete_session not implemented"
end

#delete_window(ref) ⇒ Object

Raises:

  • (NoMethodError)


36
37
38
# File 'lib/sentiero/store/session_store.rb', line 36

def delete_window(ref)
  raise NoMethodError, "#{self.class}#delete_window not implemented"
end

#each_session_events(limit: nil, since: nil, until_time: nil, types: nil) ⇒ Object

Yields [session_summary_hash, window_id, events_array] per window, newest sessions first, capped at limit. Built from list_sessions/get_session/ get_events so every backend gets it free; stores may override.

types: (integer rrweb event types) narrows only the yielded events arrays. Summaries and the yielded window set always describe the whole session, and carry :window_starts (=> first event timestamp) so replay offsets stay anchored when a window's first event is filtered out.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/sentiero/store/session_store.rb', line 54

def each_session_events(limit: nil, since: nil, until_time: nil, types: nil)
  unless block_given?
    return enum_for(:each_session_events, limit: limit, since: since, until_time: until_time, types: types)
  end

  cap = limit || limits.analytics_max_scan_sessions
  sessions = list_sessions(limit: cap, since: since, until_time: until_time)

  sessions.each do |summary|
    session = get_session(summary[:session_id])
    next unless session

    windows = session[:windows] || []
    summary = summary.merge(
      window_starts: windows.to_h { |window| [window[:window_id], window[:first_event_at]] }
    )
    windows.each do |window|
      window_id = window[:window_id]
      events = get_events(WindowRef.new(summary[:session_id], window_id))
      events = events.select { |event| types.include?(event["type"]) } if types
      yield summary, window_id, events
    end
  end
end

#event_counts_by_day(limit: nil, since: nil, until_time: nil) ⇒ Object

(UTC day of the ms timestamp) => count; events without a numeric timestamp are skipped. SQLite overrides with a payload-free aggregate.



96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/sentiero/store/session_store.rb', line 96

def event_counts_by_day(limit: nil, since: nil, until_time: nil)
  counts = {}
  each_session_events(limit: limit, since: since, until_time: until_time) do |_summary, _window_id, events|
    events.each do |event|
      timestamp = event["timestamp"]
      next unless timestamp.is_a?(Numeric)

      day = Time.at(timestamp / 1000.0).utc.to_date.to_s
      counts[day] = (counts[day] || 0) + 1
    end
  end
  counts
end

#event_type_counts(limit: nil, since: nil, until_time: nil) ⇒ Object

=> count; non-integer client "type" values tally under nil. SQLite overrides with a payload-free aggregate.



81
82
83
84
85
86
87
88
89
90
91
# File 'lib/sentiero/store/session_store.rb', line 81

def event_type_counts(limit: nil, since: nil, until_time: nil)
  counts = {}
  each_session_events(limit: limit, since: since, until_time: until_time) do |_summary, _window_id, events|
    events.each do |event|
      type = event["type"]
      key = type.is_a?(Integer) ? type : nil
      counts[key] = (counts[key] || 0) + 1
    end
  end
  counts
end

#get_events(ref, after: nil, limit: nil) ⇒ Object

Raises:

  • (NoMethodError)


28
29
30
# File 'lib/sentiero/store/session_store.rb', line 28

def get_events(ref, after: nil, limit: nil)
  raise NoMethodError, "#{self.class}#get_events not implemented"
end

#get_session(session_id) ⇒ Object

Raises:

  • (NoMethodError)


24
25
26
# File 'lib/sentiero/store/session_store.rb', line 24

def get_session(session_id)
  raise NoMethodError, "#{self.class}#get_session not implemented"
end

#list_sessions(limit:, offset: 0, since: nil, until_time: nil, sort_by: nil, search: nil, min_duration_ms: nil, min_events: nil) ⇒ Object

min_duration_ms filters on the event-timestamp span (last_event_at - first_event_at, milliseconds); sessions without event timestamps fail an active duration filter. Both thresholds are inclusive and apply before limit/offset.

Raises:

  • (NoMethodError)


19
20
21
22
# File 'lib/sentiero/store/session_store.rb', line 19

def list_sessions(limit:, offset: 0, since: nil, until_time: nil, sort_by: nil, search: nil,
  min_duration_ms: nil, min_events: nil)
  raise NoMethodError, "#{self.class}#list_sessions not implemented"
end

#purge_older_than(seconds) ⇒ Object

Deletes every session whose updated_at is older than seconds ago, returning the count. Built from list_sessions + delete_session so every backend gets it free; stores may override with a direct query.

list_sessions is newest-first, so stale sessions are the last ones reached: we page through the whole store by advancing an offset (not just re-reading the first batch) and delete only after the full scan, so deletions don't shift the pages we're still walking.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/sentiero/store/session_store.rb', line 118

def purge_older_than(seconds)
  cutoff = Time.now.to_f - seconds
  batch_size = limits.analytics_max_scan_sessions
  stale = []
  offset = 0

  loop do
    summaries = list_sessions(limit: batch_size, offset: offset)
    break if summaries.empty?

    stale.concat(
      summaries
        .select { |summary| summary[:updated_at] < cutoff }
        .map { |summary| summary[:session_id] }
    )
    break if summaries.size < batch_size

    offset += batch_size
  end

  stale.each { |session_id| delete_session(session_id) }
  stale.size
end

#save_events(ref, events) ⇒ Object

Raises:

  • (NoMethodError)


11
12
13
# File 'lib/sentiero/store/session_store.rb', line 11

def save_events(ref, events)
  raise NoMethodError, "#{self.class}#save_events not implemented"
end

#save_metadata(session_id, metadata) ⇒ Object

Optional; default is a no-op so custom stores keep working without it.



41
42
43
# File 'lib/sentiero/store/session_store.rb', line 41

def (session_id, )
  nil
end