mbuzz 0.7.0 → 0.7.2
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/README.md +57 -0
- data/lib/mbuzz/client/session_request.rb +75 -0
- data/lib/mbuzz/client.rb +12 -0
- data/lib/mbuzz/current.rb +29 -0
- data/lib/mbuzz/middleware/tracking.rb +103 -2
- data/lib/mbuzz/version.rb +1 -1
- data/lib/mbuzz.rb +63 -15
- data/lib/specs/v0.7.0_deterministic_sessions.md +8 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d8514076473bbc4fa543510e84789f8cb91ccc99e916b3cef4f4928aaf6baf5
|
|
4
|
+
data.tar.gz: 159074f0dbac29eb6b721bab4288ce976e4e4361ba503caffc36f37e79cf315e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d515f639e577a68d9daa8afe13de92dec0a5556cfad2e335ea482f7db0240ff996dd1fb6bbf3388e7f632cb8300e439028091cf1c551627c9be1c22e73ca666
|
|
7
|
+
data.tar.gz: 9fbe4999f2ce0855d83847d61b4684f30217c01c9074df4cced51e4083167cf3ca24e0e5223f2e57fd859aab6d88f60a68a0adc3074bd0a7f4356c0f9d3e5116
|
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:
|
|
@@ -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
|
|
@@ -0,0 +1,29 @@
|
|
|
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 :session_id
|
|
25
|
+
attribute :user_id
|
|
26
|
+
attribute :ip
|
|
27
|
+
attribute :user_agent
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -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,12 +18,20 @@ 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
|
|
|
24
|
+
store_in_current_attributes(context, request)
|
|
25
|
+
|
|
26
|
+
create_session_async(context, request) if context[:new_session]
|
|
27
|
+
|
|
21
28
|
RequestContext.with_context(request: request) do
|
|
22
29
|
status, headers, body = @app.call(env)
|
|
23
30
|
set_visitor_cookie(headers, context, request)
|
|
31
|
+
set_session_cookie(headers, context, request)
|
|
24
32
|
[status, headers, body]
|
|
33
|
+
ensure
|
|
34
|
+
reset_current_attributes
|
|
25
35
|
end
|
|
26
36
|
end
|
|
27
37
|
|
|
@@ -45,10 +55,26 @@ module Mbuzz
|
|
|
45
55
|
|
|
46
56
|
# Build all request-specific context as a frozen hash
|
|
47
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)
|
|
48
60
|
def build_request_context(request)
|
|
61
|
+
existing_session_id = session_id_from_cookie(request)
|
|
62
|
+
new_session = existing_session_id.nil?
|
|
63
|
+
ip = extract_ip(request)
|
|
64
|
+
user_agent = request.user_agent.to_s
|
|
65
|
+
|
|
49
66
|
{
|
|
50
67
|
visitor_id: resolve_visitor_id(request),
|
|
51
|
-
|
|
68
|
+
session_id: existing_session_id || generate_session_id,
|
|
69
|
+
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
|
+
url: request.url,
|
|
74
|
+
referrer: request.referer,
|
|
75
|
+
ip: ip,
|
|
76
|
+
user_agent: user_agent,
|
|
77
|
+
device_fingerprint: Digest::SHA256.hexdigest("#{ip}|#{user_agent}")[0, 32]
|
|
52
78
|
}.freeze
|
|
53
79
|
end
|
|
54
80
|
|
|
@@ -60,11 +86,47 @@ module Mbuzz
|
|
|
60
86
|
request.cookies[VISITOR_COOKIE_NAME]
|
|
61
87
|
end
|
|
62
88
|
|
|
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
|
+
|
|
63
97
|
def user_id_from_session(request)
|
|
64
98
|
request.session[SESSION_USER_ID_KEY] if request.session
|
|
65
99
|
end
|
|
66
100
|
|
|
67
|
-
#
|
|
101
|
+
# Session creation - async to not block request
|
|
102
|
+
# IMPORTANT: This runs in a background thread. All data must come from
|
|
103
|
+
# the context hash, NOT from the request object (which may be invalid)
|
|
104
|
+
|
|
105
|
+
def create_session_async(context, _request)
|
|
106
|
+
Thread.new do
|
|
107
|
+
create_session(context)
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
log_error("Session creation failed: #{e.message}") if Mbuzz.config.debug
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def create_session(context)
|
|
114
|
+
Client.session(
|
|
115
|
+
visitor_id: context[:visitor_id],
|
|
116
|
+
session_id: context[:session_id],
|
|
117
|
+
url: context[:url],
|
|
118
|
+
referrer: context[:referrer],
|
|
119
|
+
device_fingerprint: context[:device_fingerprint]
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def log_error(message)
|
|
124
|
+
return unless defined?(Rails) && Rails.logger
|
|
125
|
+
|
|
126
|
+
Rails.logger.error("[Mbuzz] #{message}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Cookie setting - visitor and session cookies
|
|
68
130
|
|
|
69
131
|
def set_visitor_cookie(headers, context, request)
|
|
70
132
|
Rack::Utils.set_cookie_header!(
|
|
@@ -74,6 +136,14 @@ module Mbuzz
|
|
|
74
136
|
)
|
|
75
137
|
end
|
|
76
138
|
|
|
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
|
+
|
|
77
147
|
def visitor_cookie_options(context, request)
|
|
78
148
|
base_cookie_options(request).merge(
|
|
79
149
|
value: context[:visitor_id],
|
|
@@ -81,6 +151,13 @@ module Mbuzz
|
|
|
81
151
|
)
|
|
82
152
|
end
|
|
83
153
|
|
|
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
|
+
|
|
84
161
|
def base_cookie_options(request)
|
|
85
162
|
options = {
|
|
86
163
|
path: VISITOR_COOKIE_PATH,
|
|
@@ -90,6 +167,30 @@ module Mbuzz
|
|
|
90
167
|
options[:secure] = true if request.ssl?
|
|
91
168
|
options
|
|
92
169
|
end
|
|
170
|
+
|
|
171
|
+
# Store context in CurrentAttributes for background job propagation
|
|
172
|
+
def store_in_current_attributes(context, request)
|
|
173
|
+
return unless defined?(Mbuzz::Current)
|
|
174
|
+
|
|
175
|
+
Mbuzz::Current.visitor_id = context[:visitor_id]
|
|
176
|
+
Mbuzz::Current.session_id = context[:session_id]
|
|
177
|
+
Mbuzz::Current.user_id = context[:user_id]
|
|
178
|
+
Mbuzz::Current.ip = extract_ip(request)
|
|
179
|
+
Mbuzz::Current.user_agent = request.user_agent
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def reset_current_attributes
|
|
183
|
+
return unless defined?(Mbuzz::Current)
|
|
184
|
+
|
|
185
|
+
Mbuzz::Current.reset
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def extract_ip(request)
|
|
189
|
+
forwarded = request.env["HTTP_X_FORWARDED_FOR"]
|
|
190
|
+
return forwarded.split(",").first.strip if forwarded
|
|
191
|
+
|
|
192
|
+
request.ip
|
|
193
|
+
end
|
|
93
194
|
end
|
|
94
195
|
end
|
|
95
196
|
end
|
data/lib/mbuzz/version.rb
CHANGED
data/lib/mbuzz.rb
CHANGED
|
@@ -9,6 +9,9 @@ require_relative "mbuzz/client"
|
|
|
9
9
|
require_relative "mbuzz/middleware/tracking"
|
|
10
10
|
require_relative "mbuzz/controller_helpers"
|
|
11
11
|
|
|
12
|
+
# CurrentAttributes for automatic background job context propagation (Rails only)
|
|
13
|
+
require_relative "mbuzz/current" if defined?(ActiveSupport::CurrentAttributes)
|
|
14
|
+
|
|
12
15
|
require_relative "mbuzz/railtie" if defined?(Rails::Railtie)
|
|
13
16
|
|
|
14
17
|
module Mbuzz
|
|
@@ -17,15 +20,20 @@ module Mbuzz
|
|
|
17
20
|
EVENTS_PATH = "/events"
|
|
18
21
|
IDENTIFY_PATH = "/identify"
|
|
19
22
|
CONVERSIONS_PATH = "/conversions"
|
|
23
|
+
SESSIONS_PATH = "/sessions"
|
|
20
24
|
|
|
21
25
|
VISITOR_COOKIE_NAME = "_mbuzz_vid"
|
|
22
26
|
VISITOR_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 2 # 2 years
|
|
23
27
|
VISITOR_COOKIE_PATH = "/"
|
|
24
28
|
VISITOR_COOKIE_SAME_SITE = "Lax"
|
|
25
29
|
|
|
30
|
+
SESSION_COOKIE_NAME = "_mbuzz_sid"
|
|
31
|
+
SESSION_COOKIE_MAX_AGE = 30 * 60 # 30 minutes
|
|
32
|
+
|
|
26
33
|
SESSION_USER_ID_KEY = "user_id"
|
|
27
34
|
ENV_USER_ID_KEY = "mbuzz.user_id"
|
|
28
35
|
ENV_VISITOR_ID_KEY = "mbuzz.visitor_id"
|
|
36
|
+
ENV_SESSION_ID_KEY = "mbuzz.session_id"
|
|
29
37
|
|
|
30
38
|
# ============================================================================
|
|
31
39
|
# Configuration
|
|
@@ -62,18 +70,25 @@ module Mbuzz
|
|
|
62
70
|
# Context Accessors
|
|
63
71
|
# ============================================================================
|
|
64
72
|
|
|
73
|
+
# Returns visitor_id from Current (background jobs) or request context
|
|
65
74
|
def self.visitor_id
|
|
66
|
-
RequestContext.current&.request&.env&.dig(ENV_VISITOR_ID_KEY)
|
|
75
|
+
current_visitor_id || RequestContext.current&.request&.env&.dig(ENV_VISITOR_ID_KEY)
|
|
67
76
|
end
|
|
68
77
|
|
|
69
|
-
def self.
|
|
70
|
-
|
|
78
|
+
def self.user_id
|
|
79
|
+
current_user_id || RequestContext.current&.request&.env&.dig(ENV_USER_ID_KEY)
|
|
71
80
|
end
|
|
72
|
-
private_class_method :fallback_visitor_id
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
# Check Current attributes (for background job support)
|
|
83
|
+
def self.current_visitor_id
|
|
84
|
+
defined?(Current) ? Current.visitor_id : nil
|
|
85
|
+
end
|
|
86
|
+
private_class_method :current_visitor_id
|
|
87
|
+
|
|
88
|
+
def self.current_user_id
|
|
89
|
+
defined?(Current) ? Current.user_id : nil
|
|
76
90
|
end
|
|
91
|
+
private_class_method :current_user_id
|
|
77
92
|
|
|
78
93
|
# ============================================================================
|
|
79
94
|
# 4-Call Model API
|
|
@@ -82,20 +97,30 @@ module Mbuzz
|
|
|
82
97
|
# Track an event (journey step)
|
|
83
98
|
#
|
|
84
99
|
# @param event_type [String] The name of the event
|
|
100
|
+
# @param visitor_id [String, nil] Explicit visitor ID (required for background jobs)
|
|
85
101
|
# @param properties [Hash] Custom event properties (url, referrer auto-added)
|
|
86
102
|
# @param identifier [Hash, nil] Optional identifier for cross-device identity resolution
|
|
87
103
|
# @return [Hash, false] Result hash on success, false on failure
|
|
88
104
|
#
|
|
89
|
-
# @example
|
|
105
|
+
# @example Normal usage (within request context)
|
|
90
106
|
# Mbuzz.event("add_to_cart", product_id: "SKU-123", price: 49.99)
|
|
91
107
|
#
|
|
108
|
+
# @example Background job (must pass explicit visitor_id)
|
|
109
|
+
# Mbuzz.event("order_processed", visitor_id: order.mbuzz_visitor_id, order_id: order.id)
|
|
110
|
+
#
|
|
92
111
|
# @example With identifier for cross-device tracking
|
|
93
112
|
# Mbuzz.event("page_view", identifier: { email: "user@example.com" })
|
|
94
113
|
#
|
|
95
|
-
def self.event(event_type, identifier: nil, **properties)
|
|
114
|
+
def self.event(event_type, visitor_id: nil, identifier: nil, **properties)
|
|
115
|
+
resolved_visitor_id = visitor_id || self.visitor_id
|
|
116
|
+
resolved_user_id = user_id
|
|
117
|
+
|
|
118
|
+
# Must have at least one identifier
|
|
119
|
+
return false unless resolved_visitor_id || resolved_user_id
|
|
120
|
+
|
|
96
121
|
Client.track(
|
|
97
|
-
visitor_id:
|
|
98
|
-
user_id:
|
|
122
|
+
visitor_id: resolved_visitor_id,
|
|
123
|
+
user_id: resolved_user_id,
|
|
99
124
|
event_type: event_type,
|
|
100
125
|
properties: enriched_properties(properties),
|
|
101
126
|
ip: current_ip,
|
|
@@ -113,6 +138,7 @@ module Mbuzz
|
|
|
113
138
|
# Track a conversion (revenue-generating outcome)
|
|
114
139
|
#
|
|
115
140
|
# @param conversion_type [String] The type of conversion
|
|
141
|
+
# @param visitor_id [String, nil] Explicit visitor ID (required for background jobs)
|
|
116
142
|
# @param revenue [Numeric, nil] Revenue amount
|
|
117
143
|
# @param user_id [String, nil] User ID for acquisition-linked conversions
|
|
118
144
|
# @param is_acquisition [Boolean] Mark this as the acquisition conversion for this user
|
|
@@ -121,9 +147,12 @@ module Mbuzz
|
|
|
121
147
|
# @param properties [Hash] Custom properties
|
|
122
148
|
# @return [Hash, false] Result hash on success, false on failure
|
|
123
149
|
#
|
|
124
|
-
# @example Basic conversion
|
|
150
|
+
# @example Basic conversion (within request context)
|
|
125
151
|
# Mbuzz.conversion("purchase", revenue: 99.99, order_id: "ORD-123")
|
|
126
152
|
#
|
|
153
|
+
# @example Background job (must pass explicit visitor_id)
|
|
154
|
+
# Mbuzz.conversion("purchase", visitor_id: order.mbuzz_visitor_id, revenue: 99.99)
|
|
155
|
+
#
|
|
127
156
|
# @example Acquisition conversion (marks signup as THE acquisition moment)
|
|
128
157
|
# Mbuzz.conversion("signup", user_id: "user_123", is_acquisition: true)
|
|
129
158
|
#
|
|
@@ -133,9 +162,14 @@ module Mbuzz
|
|
|
133
162
|
# @example With identifier for cross-device tracking
|
|
134
163
|
# Mbuzz.conversion("purchase", identifier: { email: "user@example.com" })
|
|
135
164
|
#
|
|
136
|
-
def self.conversion(conversion_type, revenue: nil, user_id: nil, is_acquisition: false, inherit_acquisition: false, identifier: nil, **properties)
|
|
165
|
+
def self.conversion(conversion_type, visitor_id: nil, revenue: nil, user_id: nil, is_acquisition: false, inherit_acquisition: false, identifier: nil, **properties)
|
|
166
|
+
resolved_visitor_id = visitor_id || self.visitor_id
|
|
167
|
+
|
|
168
|
+
# Must have at least one identifier (visitor_id or user_id)
|
|
169
|
+
return false unless resolved_visitor_id || user_id
|
|
170
|
+
|
|
137
171
|
Client.conversion(
|
|
138
|
-
visitor_id:
|
|
172
|
+
visitor_id: resolved_visitor_id,
|
|
139
173
|
user_id: user_id,
|
|
140
174
|
conversion_type: conversion_type,
|
|
141
175
|
revenue: revenue,
|
|
@@ -181,12 +215,26 @@ module Mbuzz
|
|
|
181
215
|
private_class_method :enriched_properties
|
|
182
216
|
|
|
183
217
|
def self.current_ip
|
|
184
|
-
RequestContext.current&.ip
|
|
218
|
+
current_attributes_ip || RequestContext.current&.ip
|
|
185
219
|
end
|
|
186
220
|
private_class_method :current_ip
|
|
187
221
|
|
|
188
222
|
def self.current_user_agent
|
|
189
|
-
RequestContext.current&.user_agent
|
|
223
|
+
current_attributes_user_agent || RequestContext.current&.user_agent
|
|
190
224
|
end
|
|
191
225
|
private_class_method :current_user_agent
|
|
226
|
+
|
|
227
|
+
def self.current_attributes_ip
|
|
228
|
+
return nil unless defined?(Current)
|
|
229
|
+
|
|
230
|
+
Current.ip
|
|
231
|
+
end
|
|
232
|
+
private_class_method :current_attributes_ip
|
|
233
|
+
|
|
234
|
+
def self.current_attributes_user_agent
|
|
235
|
+
return nil unless defined?(Current)
|
|
236
|
+
|
|
237
|
+
Current.user_agent
|
|
238
|
+
end
|
|
239
|
+
private_class_method :current_attributes_user_agent
|
|
192
240
|
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.7.
|
|
4
|
+
version: 0.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- mbuzz team
|
|
@@ -44,9 +44,11 @@ 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
|
|
51
|
+
- lib/mbuzz/current.rb
|
|
50
52
|
- lib/mbuzz/middleware/tracking.rb
|
|
51
53
|
- lib/mbuzz/railtie.rb
|
|
52
54
|
- lib/mbuzz/request_context.rb
|