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 +4 -4
- data/CHANGELOG.md +20 -0
- data/lib/mbuzz/client/session_request.rb +75 -0
- data/lib/mbuzz/client.rb +12 -0
- data/lib/mbuzz/current.rb +1 -0
- data/lib/mbuzz/middleware/tracking.rb +75 -4
- data/lib/mbuzz/version.rb +1 -1
- data/lib/mbuzz.rb +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 717fff0f77150fb5f0a2ddcd60e59aaccaacdddbe41acd88ec7b953b568ec448
|
|
4
|
+
data.tar.gz: caedfee3a6e6ced3dcfadcfcce5b80a9f13679b73710f0933b6c2d8573c2accb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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
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.
|
|
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
|