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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d8514076473bbc4fa543510e84789f8cb91ccc99e916b3cef4f4928aaf6baf5
4
- data.tar.gz: 159074f0dbac29eb6b721bab4288ce976e4e4361ba503caffc36f37e79cf315e
3
+ metadata.gz: 31940df7a70689cccec128ba1f99498c1fda911604ca48ad9c243584c71ba228
4
+ data.tar.gz: e1b98becc405f8043831645c9ce6ee3010716466e2cd5c960d1ca5ac4a3d875a
5
5
  SHA512:
6
- metadata.gz: 3d515f639e577a68d9daa8afe13de92dec0a5556cfad2e335ea482f7db0240ff996dd1fb6bbf3388e7f632cb8300e439028091cf1c551627c9be1c22e73ca666
7
- data.tar.gz: 9fbe4999f2ce0855d83847d61b4684f30217c01c9074df4cced51e4083167cf3ca24e0e5223f2e57fd859aab6d88f60a68a0adc3074bd0a7f4356c0f9d3e5116
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 context[:new_session]
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
- # This ensures thread-safety by using local variables only
58
- # IMPORTANT: All values needed by async session creation must be captured here,
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: existing_session_id || generate_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 and session cookies
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbuzz
4
- VERSION = "0.7.2"
4
+ VERSION = "0.7.4"
5
5
  end
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 || user_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: 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
 
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.2
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - mbuzz team