mbuzz 0.7.2 → 0.7.4
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 +27 -0
- data/lib/mbuzz/middleware/tracking.rb +33 -36
- data/lib/mbuzz/version.rb +1 -1
- data/lib/mbuzz.rb +15 -6
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31940df7a70689cccec128ba1f99498c1fda911604ca48ad9c243584c71ba228
|
|
4
|
+
data.tar.gz: e1b98becc405f8043831645c9ce6ee3010716466e2cd5c960d1ca5ac4a3d875a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 80ca2e08d316b5b06526dc2375ccede006ef8e369c6a796cec73182aacca047f5874eb76e1de7e3612298379e8ebd3cb7cdc75bb45f9f3f8f43b410d5ed4f11b
|
|
7
|
+
data.tar.gz: 0ce7d660c3b17d2fa26ab14acb79c8942cbc4caacc40690b4fe772a7b1fb92d45a4ae59a4dfee32e6836535d930b4b49d2fbb88d72c7f59884e1924347ee14c2
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ 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.4] - 2026-02-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`conversion()` now resolves `user_id` from context** — the `user_id: nil` parameter was shadowing `self.user_id`, preventing the identify → convert flow from working. `event()` was not affected.
|
|
13
|
+
- **`identify()` now stores `user_id` in context** — after a successful API call, `user_id` is written to `Current.user_id` and `request.env` so that subsequent `conversion()` calls in the same request can resolve it.
|
|
14
|
+
|
|
15
|
+
## [0.7.3] - 2026-02-02
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
- **Session cookie removed** — `_mbuzz_sid` cookie is no longer set or read. Sessions are fully server-side.
|
|
20
|
+
- **`SESSION_COOKIE_NAME` and `SESSION_COOKIE_MAX_AGE` constants removed** from `Mbuzz` module.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **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.
|
|
25
|
+
- Turbo Frame, htmx, Unpoly, and XHR sub-requests no longer create spurious sessions.
|
|
26
|
+
- Prefetches and iframe loads are correctly filtered out.
|
|
27
|
+
- New public methods on `Mbuzz::Middleware::Tracking`: `should_create_session?`, `sec_fetch_headers?`, `page_navigation?`, `framework_sub_request?`
|
|
28
|
+
|
|
29
|
+
### Migration Guide
|
|
30
|
+
|
|
31
|
+
1. Remove any code that reads or depends on the `_mbuzz_sid` cookie.
|
|
32
|
+
2. Remove references to `Mbuzz::SESSION_COOKIE_NAME` or `Mbuzz::SESSION_COOKIE_MAX_AGE`.
|
|
33
|
+
3. Visitor cookie (`_mbuzz_vid`) is unaffected — still set on every request.
|
|
34
|
+
|
|
8
35
|
## [0.7.0] - 2026-01-09
|
|
9
36
|
|
|
10
37
|
### Breaking Changes
|
|
@@ -23,12 +23,11 @@ module Mbuzz
|
|
|
23
23
|
|
|
24
24
|
store_in_current_attributes(context, request)
|
|
25
25
|
|
|
26
|
-
create_session_async(context, request) if
|
|
26
|
+
create_session_async(context, request) if should_create_session?(env)
|
|
27
27
|
|
|
28
28
|
RequestContext.with_context(request: request) do
|
|
29
29
|
status, headers, body = @app.call(env)
|
|
30
30
|
set_visitor_cookie(headers, context, request)
|
|
31
|
-
set_session_cookie(headers, context, request)
|
|
32
31
|
[status, headers, body]
|
|
33
32
|
ensure
|
|
34
33
|
reset_current_attributes
|
|
@@ -51,25 +50,46 @@ module Mbuzz
|
|
|
51
50
|
Mbuzz.config.all_skip_extensions.any? { |ext| path.end_with?(ext) }
|
|
52
51
|
end
|
|
53
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
|
+
|
|
54
80
|
private
|
|
55
81
|
|
|
56
|
-
# Build all request-specific context as a frozen hash
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# NOT accessed from the request object in the background thread (see #create_session)
|
|
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.
|
|
60
85
|
def build_request_context(request)
|
|
61
|
-
existing_session_id = session_id_from_cookie(request)
|
|
62
|
-
new_session = existing_session_id.nil?
|
|
63
86
|
ip = extract_ip(request)
|
|
64
87
|
user_agent = request.user_agent.to_s
|
|
65
88
|
|
|
66
89
|
{
|
|
67
90
|
visitor_id: resolve_visitor_id(request),
|
|
68
|
-
session_id:
|
|
91
|
+
session_id: SecureRandom.uuid,
|
|
69
92
|
user_id: user_id_from_session(request),
|
|
70
|
-
new_session: new_session,
|
|
71
|
-
# Session creation data - captured here for thread-safety
|
|
72
|
-
# Background thread must NOT read from request object
|
|
73
93
|
url: request.url,
|
|
74
94
|
referrer: request.referer,
|
|
75
95
|
ip: ip,
|
|
@@ -86,14 +106,6 @@ module Mbuzz
|
|
|
86
106
|
request.cookies[VISITOR_COOKIE_NAME]
|
|
87
107
|
end
|
|
88
108
|
|
|
89
|
-
def session_id_from_cookie(request)
|
|
90
|
-
request.cookies[SESSION_COOKIE_NAME]
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def generate_session_id
|
|
94
|
-
SecureRandom.uuid
|
|
95
|
-
end
|
|
96
|
-
|
|
97
109
|
def user_id_from_session(request)
|
|
98
110
|
request.session[SESSION_USER_ID_KEY] if request.session
|
|
99
111
|
end
|
|
@@ -126,7 +138,7 @@ module Mbuzz
|
|
|
126
138
|
Rails.logger.error("[Mbuzz] #{message}")
|
|
127
139
|
end
|
|
128
140
|
|
|
129
|
-
# Cookie setting - visitor
|
|
141
|
+
# Cookie setting - visitor identity only (sessions are server-side)
|
|
130
142
|
|
|
131
143
|
def set_visitor_cookie(headers, context, request)
|
|
132
144
|
Rack::Utils.set_cookie_header!(
|
|
@@ -136,14 +148,6 @@ module Mbuzz
|
|
|
136
148
|
)
|
|
137
149
|
end
|
|
138
150
|
|
|
139
|
-
def set_session_cookie(headers, context, request)
|
|
140
|
-
Rack::Utils.set_cookie_header!(
|
|
141
|
-
headers,
|
|
142
|
-
SESSION_COOKIE_NAME,
|
|
143
|
-
session_cookie_options(context, request)
|
|
144
|
-
)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
151
|
def visitor_cookie_options(context, request)
|
|
148
152
|
base_cookie_options(request).merge(
|
|
149
153
|
value: context[:visitor_id],
|
|
@@ -151,13 +155,6 @@ module Mbuzz
|
|
|
151
155
|
)
|
|
152
156
|
end
|
|
153
157
|
|
|
154
|
-
def session_cookie_options(context, request)
|
|
155
|
-
base_cookie_options(request).merge(
|
|
156
|
-
value: context[:session_id],
|
|
157
|
-
max_age: SESSION_COOKIE_MAX_AGE
|
|
158
|
-
)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
158
|
def base_cookie_options(request)
|
|
162
159
|
options = {
|
|
163
160
|
path: VISITOR_COOKIE_PATH,
|
data/lib/mbuzz/version.rb
CHANGED
data/lib/mbuzz.rb
CHANGED
|
@@ -27,9 +27,6 @@ module Mbuzz
|
|
|
27
27
|
VISITOR_COOKIE_PATH = "/"
|
|
28
28
|
VISITOR_COOKIE_SAME_SITE = "Lax"
|
|
29
29
|
|
|
30
|
-
SESSION_COOKIE_NAME = "_mbuzz_sid"
|
|
31
|
-
SESSION_COOKIE_MAX_AGE = 30 * 60 # 30 minutes
|
|
32
|
-
|
|
33
30
|
SESSION_USER_ID_KEY = "user_id"
|
|
34
31
|
ENV_USER_ID_KEY = "mbuzz.user_id"
|
|
35
32
|
ENV_VISITOR_ID_KEY = "mbuzz.visitor_id"
|
|
@@ -164,13 +161,14 @@ module Mbuzz
|
|
|
164
161
|
#
|
|
165
162
|
def self.conversion(conversion_type, visitor_id: nil, revenue: nil, user_id: nil, is_acquisition: false, inherit_acquisition: false, identifier: nil, **properties)
|
|
166
163
|
resolved_visitor_id = visitor_id || self.visitor_id
|
|
164
|
+
resolved_user_id = user_id || self.user_id
|
|
167
165
|
|
|
168
166
|
# Must have at least one identifier (visitor_id or user_id)
|
|
169
|
-
return false unless resolved_visitor_id ||
|
|
167
|
+
return false unless resolved_visitor_id || resolved_user_id
|
|
170
168
|
|
|
171
169
|
Client.conversion(
|
|
172
170
|
visitor_id: resolved_visitor_id,
|
|
173
|
-
user_id:
|
|
171
|
+
user_id: resolved_user_id,
|
|
174
172
|
conversion_type: conversion_type,
|
|
175
173
|
revenue: revenue,
|
|
176
174
|
is_acquisition: is_acquisition,
|
|
@@ -196,17 +194,28 @@ module Mbuzz
|
|
|
196
194
|
# Mbuzz.identify("user_123", visitor_id: "abc123...", traits: { email: "jane@example.com" })
|
|
197
195
|
#
|
|
198
196
|
def self.identify(user_id, traits: {}, visitor_id: nil)
|
|
199
|
-
Client.identify(
|
|
197
|
+
result = Client.identify(
|
|
200
198
|
user_id: user_id,
|
|
201
199
|
visitor_id: visitor_id || self.visitor_id,
|
|
202
200
|
traits: traits
|
|
203
201
|
)
|
|
202
|
+
|
|
203
|
+
store_user_id_in_context(user_id) if result
|
|
204
|
+
|
|
205
|
+
result
|
|
204
206
|
end
|
|
205
207
|
|
|
206
208
|
# ============================================================================
|
|
207
209
|
# Private Helpers
|
|
208
210
|
# ============================================================================
|
|
209
211
|
|
|
212
|
+
def self.store_user_id_in_context(uid)
|
|
213
|
+
str_id = uid.to_s
|
|
214
|
+
Current.user_id = str_id if defined?(Current)
|
|
215
|
+
RequestContext.current&.request&.env&.[]=(ENV_USER_ID_KEY, str_id)
|
|
216
|
+
end
|
|
217
|
+
private_class_method :store_user_id_in_context
|
|
218
|
+
|
|
210
219
|
def self.enriched_properties(custom_properties)
|
|
211
220
|
return custom_properties unless RequestContext.current
|
|
212
221
|
|