mbuzz 0.7.1 → 0.7.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb41261b9e19c46609f4e7a8867d73b5a902acecc3db3362b4a47e2cacefa9db
4
- data.tar.gz: ce644b10fa63055065960bb72a61eb53077d4bbe110eac40ed5bd6918d2487c1
3
+ metadata.gz: 717fff0f77150fb5f0a2ddcd60e59aaccaacdddbe41acd88ec7b953b568ec448
4
+ data.tar.gz: caedfee3a6e6ced3dcfadcfcce5b80a9f13679b73710f0933b6c2d8573c2accb
5
5
  SHA512:
6
- metadata.gz: 03bed0944d6ee7286e4f002d05032c8ced19d013da1dae69d429046b1ebad510cae67510d48d9af53fac8a8832a1a1495657c6b671a16976fabb5eb8ec316f3d
7
- data.tar.gz: 4a0693caaa53219fbf4368c1b194722889c516e88eea6f7bfe781f7a44293659977e6691901f238f45ef66f08f367d674c2f22e6f8a63e22f3326368e6ae8f10
6
+ metadata.gz: ce5da1805916ac523dc3d9a40e8b3126ee8f7b1d95cc3fe042d6e800e796e38227f117edd9acb72e1ec4e357cd83d378fe87b3f87eb063c2857f267ae2508c56
7
+ data.tar.gz: 190f2ddbf15dbc17cde637ee4f8ed4d00a8b06d47f0d16fce9b54e96e67e7e8191359f57cc3a5e218c9028ccac072e14e0035a1c1cc0f5c9a4e95b3c14b73c41
data/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.3] - 2026-02-02
9
+
10
+ ### Breaking Changes
11
+
12
+ - **Session cookie removed** — `_mbuzz_sid` cookie is no longer set or read. Sessions are fully server-side.
13
+ - **`SESSION_COOKIE_NAME` and `SESSION_COOKIE_MAX_AGE` constants removed** from `Mbuzz` module.
14
+
15
+ ### Added
16
+
17
+ - **Navigation-aware session creation** — middleware now gates `POST /sessions` on real page navigations using browser-enforced `Sec-Fetch-*` headers (whitelist), with a framework-specific blacklist fallback for older browsers and bots.
18
+ - Turbo Frame, htmx, Unpoly, and XHR sub-requests no longer create spurious sessions.
19
+ - Prefetches and iframe loads are correctly filtered out.
20
+ - New public methods on `Mbuzz::Middleware::Tracking`: `should_create_session?`, `sec_fetch_headers?`, `page_navigation?`, `framework_sub_request?`
21
+
22
+ ### Migration Guide
23
+
24
+ 1. Remove any code that reads or depends on the `_mbuzz_sid` cookie.
25
+ 2. Remove references to `Mbuzz::SESSION_COOKIE_NAME` or `Mbuzz::SESSION_COOKIE_MAX_AGE`.
26
+ 3. Visitor cookie (`_mbuzz_vid`) is unaffected — still set on every request.
27
+
8
28
  ## [0.7.0] - 2026-01-09
9
29
 
10
30
  ### Breaking Changes
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbuzz
4
+ class Client
5
+ class SessionRequest
6
+ # Response keys
7
+ STATUS_KEY = "status"
8
+ VISITOR_ID_KEY = "visitor_id"
9
+ SESSION_ID_KEY = "session_id"
10
+ CHANNEL_KEY = "channel"
11
+
12
+ # Response values
13
+ ACCEPTED_STATUS = "accepted"
14
+
15
+ def initialize(visitor_id:, session_id:, url:, referrer: nil, device_fingerprint: nil, started_at: nil)
16
+ @visitor_id = visitor_id
17
+ @session_id = session_id
18
+ @url = url
19
+ @referrer = referrer
20
+ @device_fingerprint = device_fingerprint
21
+ @started_at = started_at
22
+ end
23
+
24
+ def call
25
+ return false unless valid?
26
+
27
+ parse_response(response)
28
+ rescue StandardError
29
+ false
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :visitor_id, :session_id, :url, :referrer, :device_fingerprint, :started_at
35
+
36
+ def valid?
37
+ present?(visitor_id) && present?(session_id) && present?(url)
38
+ end
39
+
40
+ def response
41
+ @response ||= Api.post_with_response(SESSIONS_PATH, { session: payload })
42
+ end
43
+
44
+ def payload
45
+ {
46
+ visitor_id: visitor_id,
47
+ session_id: session_id,
48
+ url: url,
49
+ referrer: referrer,
50
+ device_fingerprint: device_fingerprint,
51
+ started_at: started_at || Time.now.utc.iso8601
52
+ }.compact
53
+ end
54
+
55
+ def parse_response(resp)
56
+ return false unless accepted?(resp)
57
+
58
+ {
59
+ success: true,
60
+ visitor_id: resp[VISITOR_ID_KEY],
61
+ session_id: resp[SESSION_ID_KEY],
62
+ channel: resp[CHANNEL_KEY]
63
+ }
64
+ end
65
+
66
+ def accepted?(resp)
67
+ resp && resp[STATUS_KEY] == ACCEPTED_STATUS
68
+ end
69
+
70
+ def present?(value)
71
+ value && !value.to_s.strip.empty?
72
+ end
73
+ end
74
+ end
75
+ end
data/lib/mbuzz/client.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "client/track_request"
4
4
  require_relative "client/identify_request"
5
5
  require_relative "client/conversion_request"
6
+ require_relative "client/session_request"
6
7
 
7
8
  module Mbuzz
8
9
  class Client
@@ -30,5 +31,16 @@ module Mbuzz
30
31
  identifier: identifier
31
32
  ).call
32
33
  end
34
+
35
+ def self.session(visitor_id:, session_id:, url:, referrer: nil, device_fingerprint: nil, started_at: nil)
36
+ SessionRequest.new(
37
+ visitor_id: visitor_id,
38
+ session_id: session_id,
39
+ url: url,
40
+ referrer: referrer,
41
+ device_fingerprint: device_fingerprint,
42
+ started_at: started_at
43
+ ).call
44
+ end
33
45
  end
34
46
  end
data/lib/mbuzz/current.rb CHANGED
@@ -21,6 +21,7 @@ module Mbuzz
21
21
  #
22
22
  class Current < ActiveSupport::CurrentAttributes
23
23
  attribute :visitor_id
24
+ attribute :session_id
24
25
  attribute :user_id
25
26
  attribute :ip
26
27
  attribute :user_agent
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rack"
4
+ require "digest"
5
+ require "securerandom"
4
6
 
5
7
  module Mbuzz
6
8
  module Middleware
@@ -16,10 +18,13 @@ module Mbuzz
16
18
  context = build_request_context(request)
17
19
 
18
20
  env[ENV_VISITOR_ID_KEY] = context[:visitor_id]
21
+ env[ENV_SESSION_ID_KEY] = context[:session_id]
19
22
  env[ENV_USER_ID_KEY] = context[:user_id]
20
23
 
21
24
  store_in_current_attributes(context, request)
22
25
 
26
+ create_session_async(context, request) if should_create_session?(env)
27
+
23
28
  RequestContext.with_context(request: request) do
24
29
  status, headers, body = @app.call(env)
25
30
  set_visitor_cookie(headers, context, request)
@@ -45,14 +50,51 @@ module Mbuzz
45
50
  Mbuzz.config.all_skip_extensions.any? { |ext| path.end_with?(ext) }
46
51
  end
47
52
 
53
+ # Navigation detection — only create sessions for real page navigations.
54
+ # Sec-Fetch-* headers (browser-enforced, unforgeable) are the primary signal;
55
+ # framework-specific blacklist covers old browsers and bots.
56
+
57
+ def should_create_session?(env)
58
+ return page_navigation?(env) if sec_fetch_headers?(env)
59
+
60
+ !framework_sub_request?(env)
61
+ end
62
+
63
+ def sec_fetch_headers?(env)
64
+ !env["HTTP_SEC_FETCH_MODE"].nil?
65
+ end
66
+
67
+ def page_navigation?(env)
68
+ env["HTTP_SEC_FETCH_MODE"] == "navigate" &&
69
+ env["HTTP_SEC_FETCH_DEST"] == "document" &&
70
+ env["HTTP_SEC_PURPOSE"].nil?
71
+ end
72
+
73
+ def framework_sub_request?(env)
74
+ !env["HTTP_TURBO_FRAME"].nil? ||
75
+ !env["HTTP_HX_REQUEST"].nil? ||
76
+ !env["HTTP_X_UP_VERSION"].nil? ||
77
+ env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
78
+ end
79
+
48
80
  private
49
81
 
50
- # Build all request-specific context as a frozen hash
51
- # This ensures thread-safety by using local variables only
82
+ # Build all request-specific context as a frozen hash.
83
+ # Thread-safety: all values needed by async session creation are captured here,
84
+ # NOT accessed from the request object in the background thread.
52
85
  def build_request_context(request)
86
+ ip = extract_ip(request)
87
+ user_agent = request.user_agent.to_s
88
+
53
89
  {
54
90
  visitor_id: resolve_visitor_id(request),
55
- user_id: user_id_from_session(request)
91
+ session_id: SecureRandom.uuid,
92
+ user_id: user_id_from_session(request),
93
+ url: request.url,
94
+ referrer: request.referer,
95
+ ip: ip,
96
+ user_agent: user_agent,
97
+ device_fingerprint: Digest::SHA256.hexdigest("#{ip}|#{user_agent}")[0, 32]
56
98
  }.freeze
57
99
  end
58
100
 
@@ -68,7 +110,35 @@ module Mbuzz
68
110
  request.session[SESSION_USER_ID_KEY] if request.session
69
111
  end
70
112
 
71
- # Cookie setting - only visitor cookie (sessions are server-side)
113
+ # Session creation - async to not block request
114
+ # IMPORTANT: This runs in a background thread. All data must come from
115
+ # the context hash, NOT from the request object (which may be invalid)
116
+
117
+ def create_session_async(context, _request)
118
+ Thread.new do
119
+ create_session(context)
120
+ rescue StandardError => e
121
+ log_error("Session creation failed: #{e.message}") if Mbuzz.config.debug
122
+ end
123
+ end
124
+
125
+ def create_session(context)
126
+ Client.session(
127
+ visitor_id: context[:visitor_id],
128
+ session_id: context[:session_id],
129
+ url: context[:url],
130
+ referrer: context[:referrer],
131
+ device_fingerprint: context[:device_fingerprint]
132
+ )
133
+ end
134
+
135
+ def log_error(message)
136
+ return unless defined?(Rails) && Rails.logger
137
+
138
+ Rails.logger.error("[Mbuzz] #{message}")
139
+ end
140
+
141
+ # Cookie setting - visitor identity only (sessions are server-side)
72
142
 
73
143
  def set_visitor_cookie(headers, context, request)
74
144
  Rack::Utils.set_cookie_header!(
@@ -100,6 +170,7 @@ module Mbuzz
100
170
  return unless defined?(Mbuzz::Current)
101
171
 
102
172
  Mbuzz::Current.visitor_id = context[:visitor_id]
173
+ Mbuzz::Current.session_id = context[:session_id]
103
174
  Mbuzz::Current.user_id = context[:user_id]
104
175
  Mbuzz::Current.ip = extract_ip(request)
105
176
  Mbuzz::Current.user_agent = request.user_agent
data/lib/mbuzz/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbuzz
4
- VERSION = "0.7.1"
4
+ VERSION = "0.7.3"
5
5
  end
data/lib/mbuzz.rb CHANGED
@@ -20,6 +20,7 @@ module Mbuzz
20
20
  EVENTS_PATH = "/events"
21
21
  IDENTIFY_PATH = "/identify"
22
22
  CONVERSIONS_PATH = "/conversions"
23
+ SESSIONS_PATH = "/sessions"
23
24
 
24
25
  VISITOR_COOKIE_NAME = "_mbuzz_vid"
25
26
  VISITOR_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 2 # 2 years
@@ -29,6 +30,7 @@ module Mbuzz
29
30
  SESSION_USER_ID_KEY = "user_id"
30
31
  ENV_USER_ID_KEY = "mbuzz.user_id"
31
32
  ENV_VISITOR_ID_KEY = "mbuzz.visitor_id"
33
+ ENV_SESSION_ID_KEY = "mbuzz.session_id"
32
34
 
33
35
  # ============================================================================
34
36
  # Configuration
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbuzz
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - mbuzz team
@@ -44,6 +44,7 @@ files:
44
44
  - lib/mbuzz/client.rb
45
45
  - lib/mbuzz/client/conversion_request.rb
46
46
  - lib/mbuzz/client/identify_request.rb
47
+ - lib/mbuzz/client/session_request.rb
47
48
  - lib/mbuzz/client/track_request.rb
48
49
  - lib/mbuzz/configuration.rb
49
50
  - lib/mbuzz/controller_helpers.rb