mbuzz 0.6.8 → 0.7.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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +57 -0
- data/lib/mbuzz/client/conversion_request.rb +13 -1
- data/lib/mbuzz/client/track_request.rb +3 -3
- data/lib/mbuzz/client.rb +7 -9
- data/lib/mbuzz/controller_helpers.rb +0 -4
- data/lib/mbuzz/current.rb +28 -0
- data/lib/mbuzz/middleware/tracking.rb +30 -99
- data/lib/mbuzz/version.rb +1 -1
- data/lib/mbuzz.rb +70 -26
- data/lib/specs/v0.7.0_deterministic_sessions.md +8 -1
- metadata +2 -3
- data/lib/mbuzz/client/session_request.rb +0 -43
- data/lib/mbuzz/session/id_generator.rb +0 -33
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb41261b9e19c46609f4e7a8867d73b5a902acecc3db3362b4a47e2cacefa9db
|
|
4
|
+
data.tar.gz: ce644b10fa63055065960bb72a61eb53077d4bbe110eac40ed5bd6918d2487c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03bed0944d6ee7286e4f002d05032c8ced19d013da1dae69d429046b1ebad510cae67510d48d9af53fac8a8832a1a1495657c6b671a16976fabb5eb8ec316f3d
|
|
7
|
+
data.tar.gz: 4a0693caaa53219fbf4368c1b194722889c516e88eea6f7bfe781f7a44293659977e6691901f238f45ef66f08f367d674c2f22e6f8a63e22f3326368e6ae8f10
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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.0] - 2026-01-09
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
|
|
12
|
+
- **Session cookie removed** - SDK no longer sets or reads `_mbuzz_sid` cookie
|
|
13
|
+
- **Session ID generation removed** - Server handles all session resolution
|
|
14
|
+
- **`Mbuzz.session_id` removed** - Use server-side session resolution instead
|
|
15
|
+
- **`Mbuzz::Client.session()` removed** - Sessions are created server-side
|
|
16
|
+
- **`session_id` parameter removed from `Client.track()`** - Not needed with server-side resolution
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Cross-device identity resolution** - New `identifier` parameter for linking sessions across devices
|
|
21
|
+
- `Mbuzz.event("page_view", identifier: { email: "user@example.com" })`
|
|
22
|
+
- `Mbuzz.conversion("purchase", identifier: { email: "user@example.com" })`
|
|
23
|
+
- **Conversion fingerprint fallback** - `ip` and `user_agent` parameters on `Client.conversion()`
|
|
24
|
+
- When visitor_id is not found, server can find visitor via recent session with same fingerprint
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Simplified middleware** - Only manages visitor cookie (`_mbuzz_vid`), no session handling
|
|
29
|
+
- **Server-side session resolution** - All session creation and resolution happens on the API server
|
|
30
|
+
- Enables true 30-minute sliding windows (vs fixed time buckets)
|
|
31
|
+
- Eliminates duplicate visitor problem from concurrent Turbo/Hotwire requests
|
|
32
|
+
- Better cross-device tracking with identity resolution
|
|
33
|
+
|
|
34
|
+
### Migration Guide
|
|
35
|
+
|
|
36
|
+
1. Remove any code that reads `Mbuzz.session_id` or `_mbuzz_sid` cookie
|
|
37
|
+
2. Remove any calls to `Mbuzz::Client.session()`
|
|
38
|
+
3. Ensure `ip` and `user_agent` are passed to track/conversion calls (handled automatically if using middleware)
|
|
39
|
+
4. Optionally add `identifier` parameter for cross-device tracking
|
|
40
|
+
|
|
8
41
|
## [0.6.8] - 2025-12-30
|
|
9
42
|
|
|
10
43
|
### Added
|
data/README.md
CHANGED
|
@@ -121,6 +121,63 @@ Mbuzz.user_id # Current user ID (from session["user_id"])
|
|
|
121
121
|
Mbuzz.session_id # Current session ID (from cookie)
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
## Background Jobs
|
|
125
|
+
|
|
126
|
+
When tracking from background jobs (Sidekiq, GoodJob, etc.), there's no HTTP request context. Rails 7+ handles this automatically via `CurrentAttributes`.
|
|
127
|
+
|
|
128
|
+
### Rails 7+ (Automatic)
|
|
129
|
+
|
|
130
|
+
mbuzz uses `ActiveSupport::CurrentAttributes` which Rails automatically serializes into ActiveJob payloads:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# In your controller - just enqueue the job
|
|
134
|
+
class OrdersController < ApplicationController
|
|
135
|
+
def create
|
|
136
|
+
@order = Order.create!(order_params)
|
|
137
|
+
ProcessOrderJob.perform_later(@order.id)
|
|
138
|
+
# visitor_id is automatically captured and passed to the job
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# In your job - mbuzz just works!
|
|
143
|
+
class ProcessOrderJob < ApplicationJob
|
|
144
|
+
def perform(order_id)
|
|
145
|
+
order = Order.find(order_id)
|
|
146
|
+
# Mbuzz::Current.visitor_id was restored by Rails
|
|
147
|
+
Mbuzz.conversion("purchase", revenue: order.total)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**How it works:**
|
|
153
|
+
1. Middleware captures `visitor_id` from cookie into `Mbuzz::Current`
|
|
154
|
+
2. Controller enqueues job
|
|
155
|
+
3. Rails serializes `Mbuzz::Current` into job payload
|
|
156
|
+
4. Job runs → Rails restores `Mbuzz::Current`
|
|
157
|
+
5. `Mbuzz.conversion()` reads from `Current` - works!
|
|
158
|
+
|
|
159
|
+
### Alternative: Explicit visitor_id
|
|
160
|
+
|
|
161
|
+
For non-Rails apps or when you need more control:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# Store visitor_id on your model
|
|
165
|
+
class Order < ApplicationRecord
|
|
166
|
+
before_create { self.mbuzz_visitor_id = Mbuzz.visitor_id }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Pass explicitly in background job
|
|
170
|
+
class ProcessOrderJob
|
|
171
|
+
def perform(order_id)
|
|
172
|
+
order = Order.find(order_id)
|
|
173
|
+
Mbuzz.conversion("purchase",
|
|
174
|
+
visitor_id: order.mbuzz_visitor_id, # Explicit
|
|
175
|
+
revenue: order.total
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
124
181
|
## Rack / Sinatra Integration
|
|
125
182
|
|
|
126
183
|
For non-Rails apps, add the middleware manually:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Mbuzz
|
|
4
4
|
class Client
|
|
5
5
|
class ConversionRequest
|
|
6
|
-
def initialize(event_id:, visitor_id:, user_id:, conversion_type:, revenue:, currency:, is_acquisition:, inherit_acquisition:, properties:)
|
|
6
|
+
def initialize(event_id:, visitor_id:, user_id:, conversion_type:, revenue:, currency:, is_acquisition:, inherit_acquisition:, properties:, ip: nil, user_agent: nil, identifier: nil)
|
|
7
7
|
@event_id = event_id
|
|
8
8
|
@visitor_id = visitor_id
|
|
9
9
|
@user_id = user_id
|
|
@@ -13,6 +13,9 @@ module Mbuzz
|
|
|
13
13
|
@is_acquisition = is_acquisition
|
|
14
14
|
@inherit_acquisition = inherit_acquisition
|
|
15
15
|
@properties = properties
|
|
16
|
+
@ip = ip
|
|
17
|
+
@user_agent = user_agent
|
|
18
|
+
@identifier = identifier
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def call
|
|
@@ -51,6 +54,7 @@ module Mbuzz
|
|
|
51
54
|
base_payload
|
|
52
55
|
.merge(optional_identifiers)
|
|
53
56
|
.merge(optional_acquisition_fields)
|
|
57
|
+
.merge(fingerprint_fields)
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
def base_payload
|
|
@@ -78,6 +82,14 @@ module Mbuzz
|
|
|
78
82
|
end
|
|
79
83
|
end
|
|
80
84
|
|
|
85
|
+
def fingerprint_fields
|
|
86
|
+
{}.tap do |h|
|
|
87
|
+
h[:ip] = @ip if @ip
|
|
88
|
+
h[:user_agent] = @user_agent if @user_agent
|
|
89
|
+
h[:identifier] = @identifier if @identifier
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
81
93
|
def present?(value) = value && !value.to_s.strip.empty?
|
|
82
94
|
def hash?(value) = value.is_a?(Hash)
|
|
83
95
|
end
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
module Mbuzz
|
|
4
4
|
class Client
|
|
5
5
|
class TrackRequest
|
|
6
|
-
def initialize(user_id, visitor_id,
|
|
6
|
+
def initialize(user_id, visitor_id, event_type, properties, ip = nil, user_agent = nil, identifier = nil)
|
|
7
7
|
@user_id = user_id
|
|
8
8
|
@visitor_id = visitor_id
|
|
9
|
-
@session_id = session_id
|
|
10
9
|
@event_type = event_type
|
|
11
10
|
@properties = properties
|
|
12
11
|
@ip = ip
|
|
13
12
|
@user_agent = user_agent
|
|
13
|
+
@identifier = identifier
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def call
|
|
@@ -38,11 +38,11 @@ module Mbuzz
|
|
|
38
38
|
{
|
|
39
39
|
user_id: @user_id,
|
|
40
40
|
visitor_id: @visitor_id,
|
|
41
|
-
session_id: @session_id,
|
|
42
41
|
event_type: @event_type,
|
|
43
42
|
properties: @properties,
|
|
44
43
|
ip: @ip,
|
|
45
44
|
user_agent: @user_agent,
|
|
45
|
+
identifier: @identifier,
|
|
46
46
|
timestamp: Time.now.utc.iso8601
|
|
47
47
|
}.compact
|
|
48
48
|
end
|
data/lib/mbuzz/client.rb
CHANGED
|
@@ -3,19 +3,18 @@
|
|
|
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"
|
|
7
6
|
|
|
8
7
|
module Mbuzz
|
|
9
8
|
class Client
|
|
10
|
-
def self.track(user_id: nil, visitor_id: nil,
|
|
11
|
-
TrackRequest.new(user_id, visitor_id,
|
|
9
|
+
def self.track(user_id: nil, visitor_id: nil, event_type:, properties: {}, ip: nil, user_agent: nil, identifier: nil)
|
|
10
|
+
TrackRequest.new(user_id, visitor_id, event_type, properties, ip, user_agent, identifier).call
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def self.identify(user_id:, visitor_id: nil, traits: {})
|
|
15
14
|
IdentifyRequest.new(user_id, visitor_id, traits).call
|
|
16
15
|
end
|
|
17
16
|
|
|
18
|
-
def self.conversion(event_id: nil, visitor_id: nil, user_id: nil, conversion_type:, revenue: nil, currency: "USD", is_acquisition: false, inherit_acquisition: false, properties: {})
|
|
17
|
+
def self.conversion(event_id: nil, visitor_id: nil, user_id: nil, conversion_type:, revenue: nil, currency: "USD", is_acquisition: false, inherit_acquisition: false, properties: {}, ip: nil, user_agent: nil, identifier: nil)
|
|
19
18
|
ConversionRequest.new(
|
|
20
19
|
event_id: event_id,
|
|
21
20
|
visitor_id: visitor_id,
|
|
@@ -25,12 +24,11 @@ module Mbuzz
|
|
|
25
24
|
currency: currency,
|
|
26
25
|
is_acquisition: is_acquisition,
|
|
27
26
|
inherit_acquisition: inherit_acquisition,
|
|
28
|
-
properties: properties
|
|
27
|
+
properties: properties,
|
|
28
|
+
ip: ip,
|
|
29
|
+
user_agent: user_agent,
|
|
30
|
+
identifier: identifier
|
|
29
31
|
).call
|
|
30
32
|
end
|
|
31
|
-
|
|
32
|
-
def self.session(visitor_id:, session_id:, url:, referrer: nil, started_at: nil)
|
|
33
|
-
SessionRequest.new(visitor_id, session_id, url, referrer, started_at).call
|
|
34
|
-
end
|
|
35
33
|
end
|
|
36
34
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mbuzz
|
|
4
|
+
# CurrentAttributes for automatic background job context propagation.
|
|
5
|
+
#
|
|
6
|
+
# Rails automatically serializes CurrentAttributes into ActiveJob payloads
|
|
7
|
+
# and restores them when jobs execute. This means visitor_id captured during
|
|
8
|
+
# the original request is available in background jobs without any manual
|
|
9
|
+
# passing or database storage.
|
|
10
|
+
#
|
|
11
|
+
# How it works:
|
|
12
|
+
# 1. Middleware captures visitor_id from cookie
|
|
13
|
+
# 2. Stores in Mbuzz::Current.visitor_id
|
|
14
|
+
# 3. Controller enqueues background job
|
|
15
|
+
# 4. Rails serializes Current attributes into job payload
|
|
16
|
+
# 5. Job runs on different thread/process
|
|
17
|
+
# 6. Rails restores Current.visitor_id before job executes
|
|
18
|
+
# 7. Mbuzz.event/conversion reads from Current.visitor_id
|
|
19
|
+
#
|
|
20
|
+
# This is why customers don't need to store visitor_id in their database.
|
|
21
|
+
#
|
|
22
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
23
|
+
attribute :visitor_id
|
|
24
|
+
attribute :user_id
|
|
25
|
+
attribute :ip
|
|
26
|
+
attribute :user_agent
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -17,14 +17,15 @@ module Mbuzz
|
|
|
17
17
|
|
|
18
18
|
env[ENV_VISITOR_ID_KEY] = context[:visitor_id]
|
|
19
19
|
env[ENV_USER_ID_KEY] = context[:user_id]
|
|
20
|
-
env[ENV_SESSION_ID_KEY] = context[:session_id]
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
create_session_if_new(context, request)
|
|
21
|
+
store_in_current_attributes(context, request)
|
|
24
22
|
|
|
23
|
+
RequestContext.with_context(request: request) do
|
|
25
24
|
status, headers, body = @app.call(env)
|
|
26
|
-
|
|
25
|
+
set_visitor_cookie(headers, context, request)
|
|
27
26
|
[status, headers, body]
|
|
27
|
+
ensure
|
|
28
|
+
reset_current_attributes
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
@@ -51,9 +52,7 @@ module Mbuzz
|
|
|
51
52
|
def build_request_context(request)
|
|
52
53
|
{
|
|
53
54
|
visitor_id: resolve_visitor_id(request),
|
|
54
|
-
|
|
55
|
-
user_id: user_id_from_session(request),
|
|
56
|
-
new_session: new_session?(request)
|
|
55
|
+
user_id: user_id_from_session(request)
|
|
57
56
|
}.freeze
|
|
58
57
|
end
|
|
59
58
|
|
|
@@ -61,91 +60,15 @@ module Mbuzz
|
|
|
61
60
|
visitor_id_from_cookie(request) || Visitor::Identifier.generate
|
|
62
61
|
end
|
|
63
62
|
|
|
64
|
-
def resolve_session_id(request)
|
|
65
|
-
session_id_from_cookie(request) || generate_session_id(request)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
63
|
def visitor_id_from_cookie(request)
|
|
69
64
|
request.cookies[VISITOR_COOKIE_NAME]
|
|
70
65
|
end
|
|
71
66
|
|
|
72
|
-
def session_id_from_cookie(request)
|
|
73
|
-
request.cookies[SESSION_COOKIE_NAME]
|
|
74
|
-
end
|
|
75
|
-
|
|
76
67
|
def user_id_from_session(request)
|
|
77
68
|
request.session[SESSION_USER_ID_KEY] if request.session
|
|
78
69
|
end
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
session_id_from_cookie(request).nil?
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def generate_session_id(request)
|
|
85
|
-
existing_visitor_id = visitor_id_from_cookie(request)
|
|
86
|
-
|
|
87
|
-
if existing_visitor_id
|
|
88
|
-
Session::IdGenerator.generate_deterministic(visitor_id: existing_visitor_id)
|
|
89
|
-
else
|
|
90
|
-
Session::IdGenerator.generate_from_fingerprint(
|
|
91
|
-
client_ip: client_ip(request),
|
|
92
|
-
user_agent: user_agent(request)
|
|
93
|
-
)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def client_ip(request)
|
|
98
|
-
request.env["HTTP_X_FORWARDED_FOR"]&.split(",")&.first&.strip ||
|
|
99
|
-
request.env["HTTP_X_REAL_IP"] ||
|
|
100
|
-
request.ip ||
|
|
101
|
-
"unknown"
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def user_agent(request)
|
|
105
|
-
request.user_agent || "unknown"
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Session creation
|
|
109
|
-
|
|
110
|
-
def create_session_if_new(context, request)
|
|
111
|
-
return unless context[:new_session]
|
|
112
|
-
|
|
113
|
-
create_session_async(context, request)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def create_session_async(context, request)
|
|
117
|
-
# Capture values in local variables for thread safety
|
|
118
|
-
visitor_id = context[:visitor_id]
|
|
119
|
-
session_id = context[:session_id]
|
|
120
|
-
url = request.url
|
|
121
|
-
referrer = request.referer
|
|
122
|
-
|
|
123
|
-
Thread.new do
|
|
124
|
-
create_session(visitor_id, session_id, url, referrer)
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def create_session(visitor_id, session_id, url, referrer)
|
|
129
|
-
Client.session(
|
|
130
|
-
visitor_id: visitor_id,
|
|
131
|
-
session_id: session_id,
|
|
132
|
-
url: url,
|
|
133
|
-
referrer: referrer
|
|
134
|
-
)
|
|
135
|
-
rescue => e
|
|
136
|
-
log_session_error(e)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def log_session_error(error)
|
|
140
|
-
Mbuzz.config.logger&.error("Session creation failed: #{error.message}")
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Cookie setting
|
|
144
|
-
|
|
145
|
-
def set_cookies(headers, context, request)
|
|
146
|
-
set_visitor_cookie(headers, context, request)
|
|
147
|
-
set_session_cookie(headers, context, request)
|
|
148
|
-
end
|
|
71
|
+
# Cookie setting - only visitor cookie (sessions are server-side)
|
|
149
72
|
|
|
150
73
|
def set_visitor_cookie(headers, context, request)
|
|
151
74
|
Rack::Utils.set_cookie_header!(
|
|
@@ -155,14 +78,6 @@ module Mbuzz
|
|
|
155
78
|
)
|
|
156
79
|
end
|
|
157
80
|
|
|
158
|
-
def set_session_cookie(headers, context, request)
|
|
159
|
-
Rack::Utils.set_cookie_header!(
|
|
160
|
-
headers,
|
|
161
|
-
SESSION_COOKIE_NAME,
|
|
162
|
-
session_cookie_options(context, request)
|
|
163
|
-
)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
81
|
def visitor_cookie_options(context, request)
|
|
167
82
|
base_cookie_options(request).merge(
|
|
168
83
|
value: context[:visitor_id],
|
|
@@ -170,13 +85,6 @@ module Mbuzz
|
|
|
170
85
|
)
|
|
171
86
|
end
|
|
172
87
|
|
|
173
|
-
def session_cookie_options(context, request)
|
|
174
|
-
base_cookie_options(request).merge(
|
|
175
|
-
value: context[:session_id],
|
|
176
|
-
max_age: SESSION_COOKIE_MAX_AGE
|
|
177
|
-
)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
88
|
def base_cookie_options(request)
|
|
181
89
|
options = {
|
|
182
90
|
path: VISITOR_COOKIE_PATH,
|
|
@@ -186,6 +94,29 @@ module Mbuzz
|
|
|
186
94
|
options[:secure] = true if request.ssl?
|
|
187
95
|
options
|
|
188
96
|
end
|
|
97
|
+
|
|
98
|
+
# Store context in CurrentAttributes for background job propagation
|
|
99
|
+
def store_in_current_attributes(context, request)
|
|
100
|
+
return unless defined?(Mbuzz::Current)
|
|
101
|
+
|
|
102
|
+
Mbuzz::Current.visitor_id = context[:visitor_id]
|
|
103
|
+
Mbuzz::Current.user_id = context[:user_id]
|
|
104
|
+
Mbuzz::Current.ip = extract_ip(request)
|
|
105
|
+
Mbuzz::Current.user_agent = request.user_agent
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def reset_current_attributes
|
|
109
|
+
return unless defined?(Mbuzz::Current)
|
|
110
|
+
|
|
111
|
+
Mbuzz::Current.reset
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def extract_ip(request)
|
|
115
|
+
forwarded = request.env["HTTP_X_FORWARDED_FOR"]
|
|
116
|
+
return forwarded.split(",").first.strip if forwarded
|
|
117
|
+
|
|
118
|
+
request.ip
|
|
119
|
+
end
|
|
189
120
|
end
|
|
190
121
|
end
|
|
191
122
|
end
|
data/lib/mbuzz/version.rb
CHANGED
data/lib/mbuzz.rb
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
require_relative "mbuzz/version"
|
|
4
4
|
require_relative "mbuzz/configuration"
|
|
5
5
|
require_relative "mbuzz/visitor/identifier"
|
|
6
|
-
require_relative "mbuzz/session/id_generator"
|
|
7
6
|
require_relative "mbuzz/request_context"
|
|
8
7
|
require_relative "mbuzz/api"
|
|
9
8
|
require_relative "mbuzz/client"
|
|
10
9
|
require_relative "mbuzz/middleware/tracking"
|
|
11
10
|
require_relative "mbuzz/controller_helpers"
|
|
12
11
|
|
|
12
|
+
# CurrentAttributes for automatic background job context propagation (Rails only)
|
|
13
|
+
require_relative "mbuzz/current" if defined?(ActiveSupport::CurrentAttributes)
|
|
14
|
+
|
|
13
15
|
require_relative "mbuzz/railtie" if defined?(Rails::Railtie)
|
|
14
16
|
|
|
15
17
|
module Mbuzz
|
|
@@ -18,20 +20,15 @@ module Mbuzz
|
|
|
18
20
|
EVENTS_PATH = "/events"
|
|
19
21
|
IDENTIFY_PATH = "/identify"
|
|
20
22
|
CONVERSIONS_PATH = "/conversions"
|
|
21
|
-
SESSIONS_PATH = "/sessions"
|
|
22
23
|
|
|
23
24
|
VISITOR_COOKIE_NAME = "_mbuzz_vid"
|
|
24
25
|
VISITOR_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 2 # 2 years
|
|
25
26
|
VISITOR_COOKIE_PATH = "/"
|
|
26
27
|
VISITOR_COOKIE_SAME_SITE = "Lax"
|
|
27
28
|
|
|
28
|
-
SESSION_COOKIE_NAME = "_mbuzz_sid"
|
|
29
|
-
SESSION_COOKIE_MAX_AGE = 30 * 60 # 30 minutes
|
|
30
|
-
|
|
31
29
|
SESSION_USER_ID_KEY = "user_id"
|
|
32
30
|
ENV_USER_ID_KEY = "mbuzz.user_id"
|
|
33
31
|
ENV_VISITOR_ID_KEY = "mbuzz.visitor_id"
|
|
34
|
-
ENV_SESSION_ID_KEY = "mbuzz.session_id"
|
|
35
32
|
|
|
36
33
|
# ============================================================================
|
|
37
34
|
# Configuration
|
|
@@ -68,22 +65,25 @@ module Mbuzz
|
|
|
68
65
|
# Context Accessors
|
|
69
66
|
# ============================================================================
|
|
70
67
|
|
|
68
|
+
# Returns visitor_id from Current (background jobs) or request context
|
|
71
69
|
def self.visitor_id
|
|
72
|
-
RequestContext.current&.request&.env&.dig(ENV_VISITOR_ID_KEY)
|
|
70
|
+
current_visitor_id || RequestContext.current&.request&.env&.dig(ENV_VISITOR_ID_KEY)
|
|
73
71
|
end
|
|
74
72
|
|
|
75
|
-
def self.
|
|
76
|
-
|
|
73
|
+
def self.user_id
|
|
74
|
+
current_user_id || RequestContext.current&.request&.env&.dig(ENV_USER_ID_KEY)
|
|
77
75
|
end
|
|
78
|
-
private_class_method :fallback_visitor_id
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
# Check Current attributes (for background job support)
|
|
78
|
+
def self.current_visitor_id
|
|
79
|
+
defined?(Current) ? Current.visitor_id : nil
|
|
82
80
|
end
|
|
81
|
+
private_class_method :current_visitor_id
|
|
83
82
|
|
|
84
|
-
def self.
|
|
85
|
-
|
|
83
|
+
def self.current_user_id
|
|
84
|
+
defined?(Current) ? Current.user_id : nil
|
|
86
85
|
end
|
|
86
|
+
private_class_method :current_user_id
|
|
87
87
|
|
|
88
88
|
# ============================================================================
|
|
89
89
|
# 4-Call Model API
|
|
@@ -92,21 +92,35 @@ module Mbuzz
|
|
|
92
92
|
# Track an event (journey step)
|
|
93
93
|
#
|
|
94
94
|
# @param event_type [String] The name of the event
|
|
95
|
+
# @param visitor_id [String, nil] Explicit visitor ID (required for background jobs)
|
|
95
96
|
# @param properties [Hash] Custom event properties (url, referrer auto-added)
|
|
97
|
+
# @param identifier [Hash, nil] Optional identifier for cross-device identity resolution
|
|
96
98
|
# @return [Hash, false] Result hash on success, false on failure
|
|
97
99
|
#
|
|
98
|
-
# @example
|
|
100
|
+
# @example Normal usage (within request context)
|
|
99
101
|
# Mbuzz.event("add_to_cart", product_id: "SKU-123", price: 49.99)
|
|
100
102
|
#
|
|
101
|
-
|
|
103
|
+
# @example Background job (must pass explicit visitor_id)
|
|
104
|
+
# Mbuzz.event("order_processed", visitor_id: order.mbuzz_visitor_id, order_id: order.id)
|
|
105
|
+
#
|
|
106
|
+
# @example With identifier for cross-device tracking
|
|
107
|
+
# Mbuzz.event("page_view", identifier: { email: "user@example.com" })
|
|
108
|
+
#
|
|
109
|
+
def self.event(event_type, visitor_id: nil, identifier: nil, **properties)
|
|
110
|
+
resolved_visitor_id = visitor_id || self.visitor_id
|
|
111
|
+
resolved_user_id = user_id
|
|
112
|
+
|
|
113
|
+
# Must have at least one identifier
|
|
114
|
+
return false unless resolved_visitor_id || resolved_user_id
|
|
115
|
+
|
|
102
116
|
Client.track(
|
|
103
|
-
visitor_id:
|
|
104
|
-
|
|
105
|
-
user_id: user_id,
|
|
117
|
+
visitor_id: resolved_visitor_id,
|
|
118
|
+
user_id: resolved_user_id,
|
|
106
119
|
event_type: event_type,
|
|
107
120
|
properties: enriched_properties(properties),
|
|
108
121
|
ip: current_ip,
|
|
109
|
-
user_agent: current_user_agent
|
|
122
|
+
user_agent: current_user_agent,
|
|
123
|
+
identifier: identifier
|
|
110
124
|
)
|
|
111
125
|
end
|
|
112
126
|
|
|
@@ -119,31 +133,47 @@ module Mbuzz
|
|
|
119
133
|
# Track a conversion (revenue-generating outcome)
|
|
120
134
|
#
|
|
121
135
|
# @param conversion_type [String] The type of conversion
|
|
136
|
+
# @param visitor_id [String, nil] Explicit visitor ID (required for background jobs)
|
|
122
137
|
# @param revenue [Numeric, nil] Revenue amount
|
|
123
138
|
# @param user_id [String, nil] User ID for acquisition-linked conversions
|
|
124
139
|
# @param is_acquisition [Boolean] Mark this as the acquisition conversion for this user
|
|
125
140
|
# @param inherit_acquisition [Boolean] Inherit attribution from user's acquisition conversion
|
|
141
|
+
# @param identifier [Hash, nil] Optional identifier for cross-device identity resolution
|
|
126
142
|
# @param properties [Hash] Custom properties
|
|
127
143
|
# @return [Hash, false] Result hash on success, false on failure
|
|
128
144
|
#
|
|
129
|
-
# @example Basic conversion
|
|
145
|
+
# @example Basic conversion (within request context)
|
|
130
146
|
# Mbuzz.conversion("purchase", revenue: 99.99, order_id: "ORD-123")
|
|
131
147
|
#
|
|
148
|
+
# @example Background job (must pass explicit visitor_id)
|
|
149
|
+
# Mbuzz.conversion("purchase", visitor_id: order.mbuzz_visitor_id, revenue: 99.99)
|
|
150
|
+
#
|
|
132
151
|
# @example Acquisition conversion (marks signup as THE acquisition moment)
|
|
133
152
|
# Mbuzz.conversion("signup", user_id: "user_123", is_acquisition: true)
|
|
134
153
|
#
|
|
135
154
|
# @example Recurring revenue (inherits attribution from acquisition)
|
|
136
155
|
# Mbuzz.conversion("payment", user_id: "user_123", revenue: 49.00, inherit_acquisition: true)
|
|
137
156
|
#
|
|
138
|
-
|
|
157
|
+
# @example With identifier for cross-device tracking
|
|
158
|
+
# Mbuzz.conversion("purchase", identifier: { email: "user@example.com" })
|
|
159
|
+
#
|
|
160
|
+
def self.conversion(conversion_type, visitor_id: nil, revenue: nil, user_id: nil, is_acquisition: false, inherit_acquisition: false, identifier: nil, **properties)
|
|
161
|
+
resolved_visitor_id = visitor_id || self.visitor_id
|
|
162
|
+
|
|
163
|
+
# Must have at least one identifier (visitor_id or user_id)
|
|
164
|
+
return false unless resolved_visitor_id || user_id
|
|
165
|
+
|
|
139
166
|
Client.conversion(
|
|
140
|
-
visitor_id:
|
|
167
|
+
visitor_id: resolved_visitor_id,
|
|
141
168
|
user_id: user_id,
|
|
142
169
|
conversion_type: conversion_type,
|
|
143
170
|
revenue: revenue,
|
|
144
171
|
is_acquisition: is_acquisition,
|
|
145
172
|
inherit_acquisition: inherit_acquisition,
|
|
146
|
-
properties: enriched_properties(properties)
|
|
173
|
+
properties: enriched_properties(properties),
|
|
174
|
+
ip: current_ip,
|
|
175
|
+
user_agent: current_user_agent,
|
|
176
|
+
identifier: identifier
|
|
147
177
|
)
|
|
148
178
|
end
|
|
149
179
|
|
|
@@ -180,12 +210,26 @@ module Mbuzz
|
|
|
180
210
|
private_class_method :enriched_properties
|
|
181
211
|
|
|
182
212
|
def self.current_ip
|
|
183
|
-
RequestContext.current&.ip
|
|
213
|
+
current_attributes_ip || RequestContext.current&.ip
|
|
184
214
|
end
|
|
185
215
|
private_class_method :current_ip
|
|
186
216
|
|
|
187
217
|
def self.current_user_agent
|
|
188
|
-
RequestContext.current&.user_agent
|
|
218
|
+
current_attributes_user_agent || RequestContext.current&.user_agent
|
|
189
219
|
end
|
|
190
220
|
private_class_method :current_user_agent
|
|
221
|
+
|
|
222
|
+
def self.current_attributes_ip
|
|
223
|
+
return nil unless defined?(Current)
|
|
224
|
+
|
|
225
|
+
Current.ip
|
|
226
|
+
end
|
|
227
|
+
private_class_method :current_attributes_ip
|
|
228
|
+
|
|
229
|
+
def self.current_attributes_user_agent
|
|
230
|
+
return nil unless defined?(Current)
|
|
231
|
+
|
|
232
|
+
Current.user_agent
|
|
233
|
+
end
|
|
234
|
+
private_class_method :current_attributes_user_agent
|
|
191
235
|
end
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# mbuzz SDK v0.7.0 - Deterministic Session IDs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **⚠️ SUPERSEDED**: This spec has been replaced by server-side session resolution.
|
|
4
|
+
> See: `multibuzz/lib/specs/1_visitor_session_tracking_spec.md`
|
|
5
|
+
>
|
|
6
|
+
> **What changed**: Instead of SDKs generating deterministic session IDs using time-buckets,
|
|
7
|
+
> the server now handles all session resolution using IP + User-Agent fingerprinting with
|
|
8
|
+
> a true 30-minute sliding window. SDKs no longer manage session cookies.
|
|
9
|
+
|
|
10
|
+
**Status**: SUPERSEDED (2026-01-09)
|
|
4
11
|
**Last Updated**: 2025-12-29
|
|
5
12
|
**Breaking Change**: No (backward compatible)
|
|
6
13
|
**Affects**: All SDKs (Ruby, Python, PHP, Node)
|
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.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- mbuzz team
|
|
@@ -44,14 +44,13 @@ 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
|
|
48
47
|
- lib/mbuzz/client/track_request.rb
|
|
49
48
|
- lib/mbuzz/configuration.rb
|
|
50
49
|
- lib/mbuzz/controller_helpers.rb
|
|
50
|
+
- lib/mbuzz/current.rb
|
|
51
51
|
- lib/mbuzz/middleware/tracking.rb
|
|
52
52
|
- lib/mbuzz/railtie.rb
|
|
53
53
|
- lib/mbuzz/request_context.rb
|
|
54
|
-
- lib/mbuzz/session/id_generator.rb
|
|
55
54
|
- lib/mbuzz/version.rb
|
|
56
55
|
- lib/mbuzz/visitor/identifier.rb
|
|
57
56
|
- lib/specs/old/SPECIFICATION.md
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mbuzz
|
|
4
|
-
class Client
|
|
5
|
-
class SessionRequest
|
|
6
|
-
def initialize(visitor_id, session_id, url, referrer, started_at)
|
|
7
|
-
@visitor_id = visitor_id
|
|
8
|
-
@session_id = session_id
|
|
9
|
-
@url = url
|
|
10
|
-
@referrer = referrer
|
|
11
|
-
@started_at = started_at || Time.now.utc.iso8601
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def call
|
|
15
|
-
return false unless valid?
|
|
16
|
-
|
|
17
|
-
Api.post(SESSIONS_PATH, payload)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
attr_reader :visitor_id, :session_id, :url, :referrer, :started_at
|
|
23
|
-
|
|
24
|
-
def valid?
|
|
25
|
-
present?(visitor_id) && present?(session_id) && present?(url)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def payload
|
|
29
|
-
{
|
|
30
|
-
session: {
|
|
31
|
-
visitor_id: visitor_id,
|
|
32
|
-
session_id: session_id,
|
|
33
|
-
url: url,
|
|
34
|
-
referrer: referrer,
|
|
35
|
-
started_at: started_at
|
|
36
|
-
}.compact
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def present?(value) = value && !value.to_s.strip.empty?
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
|
|
6
|
-
module Mbuzz
|
|
7
|
-
module Session
|
|
8
|
-
class IdGenerator
|
|
9
|
-
SESSION_TIMEOUT_SECONDS = 1800
|
|
10
|
-
SESSION_ID_LENGTH = 64
|
|
11
|
-
FINGERPRINT_LENGTH = 32
|
|
12
|
-
|
|
13
|
-
class << self
|
|
14
|
-
def generate_deterministic(visitor_id:, timestamp: Time.now.to_i)
|
|
15
|
-
time_bucket = timestamp / SESSION_TIMEOUT_SECONDS
|
|
16
|
-
raw = "#{visitor_id}_#{time_bucket}"
|
|
17
|
-
Digest::SHA256.hexdigest(raw)[0, SESSION_ID_LENGTH]
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def generate_from_fingerprint(client_ip:, user_agent:, timestamp: Time.now.to_i)
|
|
21
|
-
fingerprint = Digest::SHA256.hexdigest("#{client_ip}|#{user_agent}")[0, FINGERPRINT_LENGTH]
|
|
22
|
-
time_bucket = timestamp / SESSION_TIMEOUT_SECONDS
|
|
23
|
-
raw = "#{fingerprint}_#{time_bucket}"
|
|
24
|
-
Digest::SHA256.hexdigest(raw)[0, SESSION_ID_LENGTH]
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def generate_random
|
|
28
|
-
SecureRandom.hex(32)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|