mbuzz 0.9.0 → 0.10.1

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: 81970794a962b276527d5aa05efb59703e1e3507598218c66f3eb3c238a96b6a
4
- data.tar.gz: 391e3993a7aa60f3ad5fe50dfeff1394c9f3282538d3074de12f7a7a284d8e15
3
+ metadata.gz: 71d8b00de3facb7fd1290c03f1493edf09586333ad6ea44e1de03ebba375805f
4
+ data.tar.gz: 69eac0945b4b2630038753770fd398154cc1d407e15e9e8ff0d1770fb53bf95d
5
5
  SHA512:
6
- metadata.gz: 2187c1959502e1dcc183e6f3fd2d77b46b90e87427bb79078c04cac35715891ec89cf274514cf8e8f327de2d8176e0d381875c961b9f61b37fee18f313c8ff9e
7
- data.tar.gz: eb34d5989f34c7e83d5315c57000dff733af12a26494255817a414002d0448a1d8ee8f71143a94348f1b5118df743acf97cdeffd2347584e0cbe561b73b396e3
6
+ metadata.gz: 5f0a99184a1860342c7088c1642495d11db115ecbd7571bfffd6f29ea8fc770d6d0ea3d473f1ae7a6681eadb19d8b387ce86968df369d5e785301a1f157b2ad3
7
+ data.tar.gz: 3ba3d4419fa579e5840a46e398202637f4d7f5b119fbc2815b1cac5db1504f4d0f69a5d68481ad1075bb875d42e0121aa9f06696bf9cc9a254accb9c7a1a0aab
data/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ 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.10.1] - 2026-09-17
9
+
10
+ ### Fixed — the visitor's IP now reaches mbuzz on session create
11
+
12
+ - **`POST /api/v1/sessions` now carries `ip`.** The middleware already extracted the visitor's
13
+ address — it is half of `device_fingerprint = SHA256(ip|user_agent)` — and then dropped it at
14
+ three points on the way out: `Client.session` had no `ip:` keyword, `SessionRequest#payload`
15
+ omitted it, and `Middleware::Tracking`'s own call site omitted it too. Events and conversions have
16
+ always sent one; only sessions did not.
17
+ - **Why it matters.** mbuzz sends Meta's Conversions API `client_ip_address`, which Meta documents
18
+ as **required** for website events, and a session with no address means the events resolved
19
+ through it carry none either. Upgrading improves Event Match Quality for anyone using conversion
20
+ feedback; it changes nothing else.
21
+ - **No API change.** `ip:` is an optional keyword on `Client.session` and both middlewares fill it
22
+ automatically. Existing callers need no edit.
23
+
24
+ Found by the SDK integration harness rather than by unit tests: every layer was correct on its own,
25
+ and only a real request through a real app could tell the visitor's address from the app server's.
26
+
27
+ ## [0.10.0] - 2026-09-08
28
+
29
+ ### BREAKING — the page snippet is now required
30
+
31
+ - **A page response no longer sets the visitor cookie.** Only `POST /_mbuzz/session` mints, and the inline snippet in the README's "Full-page caching" section is what calls it. **Upgrading without adding that snippet stops tracking entirely** — no cookie is ever minted, so no visitor exists and every event is dropped.
32
+
33
+ ### Fixed
34
+
35
+ - **A cached page no longer hands every visitor the same id.** The page response carried a `Set-Cookie`, and a full-page cache stored it and replayed it to everyone — so unrelated people merged into a single journey. Corruption rather than loss: every row still exists, each attributed to the wrong person, and nothing looks missing. The page path now uses only the cookie the browser already holds; the uncached session endpoint is the sole place a visitor is minted.
36
+
37
+ Found by the cache harness at `sdk_integration_tests/scenarios/page_cache_test.rb`, and it could not have been found any other way: in isolation "reuse the cookie the browser presents" is correct — it is what lets a returning visitor keep their id. The bug exists only once a cache has handed one cookie to many people. The WordPress plugin reached the same conclusion first (`CookieBootstrap::CONTEXT_PAGE`).
38
+
8
39
  ## [0.9.0] - 2026-09-02
9
40
 
10
41
  ### Added
data/README.md CHANGED
@@ -193,16 +193,22 @@ use Mbuzz::Middleware::Tracking
193
193
  run MyApp
194
194
  ```
195
195
 
196
- ## Full-page caching
196
+ ## Full-page caching — the snippet below is REQUIRED
197
197
 
198
- If pages are served from a full-page cache (Cloudflare, Varnish, nginx, Rack::Cache, a CDN),
199
- the cache answers the request **without entering the Rack stack**, so the tracking middleware
200
- never runs, no visitor cookie is set, and every later event is dropped for having no one to
201
- attribute it to. The page renders perfectly and nothing is logged — the failure is silent.
198
+ **Add this to your layout or nothing is tracked.** Since 0.10.0 a page response never sets the
199
+ visitor cookie: only `POST /_mbuzz/session` mints, and the snippet is what calls it.
202
200
 
203
- `Mbuzz::Middleware::SessionEndpoint` fixes this. It answers `POST /_mbuzz/session`, a path
204
- caches don't store, and the **server** sets the cookie on that response. Rails mounts it for
205
- you; Rack and Sinatra apps add it ahead of `Tracking` as shown above.
201
+ A page response can be stored by a full-page cache (Cloudflare, Varnish, nginx, Rack::Cache, a
202
+ CDN) and replayed to every visitor. A `Set-Cookie` sitting in that cache hands everyone the
203
+ *first* visitor's id, so unrelated people merge into one journey — corruption rather than loss,
204
+ since every row exists and is simply attributed to the wrong person. Minting only on a response
205
+ no cache stores is the only way to prevent it.
206
+
207
+ The same endpoint solves the original problem too: a cached page never enters the Rack stack,
208
+ so the tracking middleware cannot run — but this one request always reaches it.
209
+
210
+ `Mbuzz::Middleware::SessionEndpoint` answers `POST /_mbuzz/session`. Rails mounts it for you;
211
+ Rack and Sinatra apps add it ahead of `Tracking` as shown above.
206
212
 
207
213
  Then call it once per page, from your layout:
208
214
 
@@ -12,7 +12,8 @@ module Mbuzz
12
12
  # Response values
13
13
  ACCEPTED_STATUS = "accepted"
14
14
 
15
- def initialize(visitor_id:, session_id:, url:, referrer: nil, device_fingerprint: nil, user_agent: nil, started_at: nil)
15
+ def initialize(visitor_id:, session_id:, url:, referrer: nil, device_fingerprint: nil,
16
+ user_agent: nil, started_at: nil, ip: nil)
16
17
  @visitor_id = visitor_id
17
18
  @session_id = session_id
18
19
  @url = url
@@ -20,6 +21,7 @@ module Mbuzz
20
21
  @device_fingerprint = device_fingerprint
21
22
  @user_agent = user_agent
22
23
  @started_at = started_at
24
+ @ip = ip
23
25
  end
24
26
 
25
27
  def call
@@ -32,7 +34,8 @@ module Mbuzz
32
34
 
33
35
  private
34
36
 
35
- attr_reader :visitor_id, :session_id, :url, :referrer, :device_fingerprint, :user_agent, :started_at
37
+ attr_reader :visitor_id, :session_id, :url, :referrer, :device_fingerprint, :user_agent,
38
+ :started_at, :ip
36
39
 
37
40
  def valid?
38
41
  present?(visitor_id) && present?(session_id) && present?(url)
@@ -42,6 +45,11 @@ module Mbuzz
42
45
  @response ||= Api.post_with_response(SESSIONS_PATH, { session: payload })
43
46
  end
44
47
 
48
+ # `ip` is the visitor's address, which the middleware already extracted to
49
+ # compute `device_fingerprint` (SHA256(ip|user_agent)). It was computed
50
+ # and then dropped here until 2026-09-17: mbuzz needs it for Meta's
51
+ # `client_ip_address`, which Meta documents as required for website
52
+ # events, and the events/conversions payloads have always carried it.
45
53
  def payload
46
54
  {
47
55
  visitor_id: visitor_id,
@@ -50,6 +58,7 @@ module Mbuzz
50
58
  referrer: referrer,
51
59
  device_fingerprint: device_fingerprint,
52
60
  user_agent: user_agent,
61
+ ip: ip,
53
62
  started_at: started_at || Time.now.utc.iso8601
54
63
  }.compact
55
64
  end
data/lib/mbuzz/client.rb CHANGED
@@ -32,7 +32,8 @@ module Mbuzz
32
32
  ).call
33
33
  end
34
34
 
35
- def self.session(visitor_id:, session_id:, url:, referrer: nil, device_fingerprint: nil, user_agent: nil, started_at: nil)
35
+ def self.session(visitor_id:, session_id:, url:, referrer: nil, device_fingerprint: nil,
36
+ user_agent: nil, started_at: nil, ip: nil)
36
37
  SessionRequest.new(
37
38
  visitor_id: visitor_id,
38
39
  session_id: session_id,
@@ -40,7 +41,8 @@ module Mbuzz
40
41
  referrer: referrer,
41
42
  device_fingerprint: device_fingerprint,
42
43
  user_agent: user_agent,
43
- started_at: started_at
44
+ started_at: started_at,
45
+ ip: ip
44
46
  ).call
45
47
  end
46
48
  end
@@ -93,7 +93,8 @@ module Mbuzz
93
93
  url: context[:url],
94
94
  referrer: context[:referrer],
95
95
  device_fingerprint: context[:device_fingerprint],
96
- user_agent: context[:user_agent]
96
+ user_agent: context[:user_agent],
97
+ ip: context[:ip]
97
98
  )
98
99
  rescue StandardError => e
99
100
  log_error("Session creation failed: #{e.message}") if Mbuzz.config.debug
@@ -15,6 +15,16 @@ module Mbuzz
15
15
  return @app.call(env) if skip_request?(env)
16
16
 
17
17
  request = Rack::Request.new(env)
18
+
19
+ # Only a visitor the browser already holds. This response may be stored
20
+ # by a full-page cache and replayed to everyone, so minting here would
21
+ # put a Set-Cookie in that cache and hand every later visitor the same
22
+ # id — unrelated people merged into one journey. Corruption, not loss:
23
+ # every row still exists, each attributed to the wrong person, and
24
+ # nothing looks missing. A first-time visitor is established a moment
25
+ # later by Middleware::SessionEndpoint, whose POST no cache stores.
26
+ return @app.call(env) unless visitor_id_from_cookie(request)
27
+
18
28
  context = build_request_context(request)
19
29
 
20
30
  env[ENV_VISITOR_ID_KEY] = context[:visitor_id]
@@ -26,9 +36,7 @@ module Mbuzz
26
36
  create_session_async(context, request) if should_create_session?(env)
27
37
 
28
38
  RequestContext.with_context(request: request) do
29
- status, headers, body = @app.call(env)
30
- set_visitor_cookie(headers, context, request)
31
- [status, headers, body]
39
+ @app.call(env)
32
40
  ensure
33
41
  reset_current_attributes
34
42
  end
@@ -87,7 +95,7 @@ module Mbuzz
87
95
  user_agent = request.user_agent.to_s
88
96
 
89
97
  {
90
- visitor_id: resolve_visitor_id(request),
98
+ visitor_id: visitor_id_from_cookie(request),
91
99
  session_id: SecureRandom.uuid,
92
100
  user_id: user_id_from_session(request),
93
101
  url: request.url,
@@ -98,10 +106,6 @@ module Mbuzz
98
106
  }.freeze
99
107
  end
100
108
 
101
- def resolve_visitor_id(request)
102
- visitor_id_from_cookie(request) || Visitor::Identifier.generate
103
- end
104
-
105
109
  def visitor_id_from_cookie(request)
106
110
  request.cookies[VISITOR_COOKIE_NAME]
107
111
  end
@@ -129,7 +133,8 @@ module Mbuzz
129
133
  url: context[:url],
130
134
  referrer: context[:referrer],
131
135
  device_fingerprint: context[:device_fingerprint],
132
- user_agent: context[:user_agent]
136
+ user_agent: context[:user_agent],
137
+ ip: context[:ip]
133
138
  )
134
139
  end
135
140
 
@@ -139,33 +144,6 @@ module Mbuzz
139
144
  Rails.logger.error("[Mbuzz] #{message}")
140
145
  end
141
146
 
142
- # Cookie setting - visitor identity only (sessions are server-side)
143
-
144
- def set_visitor_cookie(headers, context, request)
145
- Rack::Utils.set_cookie_header!(
146
- headers,
147
- VISITOR_COOKIE_NAME,
148
- visitor_cookie_options(context, request)
149
- )
150
- end
151
-
152
- def visitor_cookie_options(context, request)
153
- base_cookie_options(request).merge(
154
- value: context[:visitor_id],
155
- max_age: VISITOR_COOKIE_MAX_AGE
156
- )
157
- end
158
-
159
- def base_cookie_options(request)
160
- options = {
161
- path: VISITOR_COOKIE_PATH,
162
- httponly: true,
163
- same_site: VISITOR_COOKIE_SAME_SITE
164
- }
165
- options[:secure] = true if request.ssl?
166
- options
167
- end
168
-
169
147
  # Store context in CurrentAttributes for background job propagation
170
148
  def store_in_current_attributes(context, request)
171
149
  return unless defined?(Mbuzz::Current)
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.9.0"
4
+ VERSION = "0.10.1"
5
5
  end
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.9.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - mbuzz team