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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f4cc2a9e5dd77624d8c645e32752658da3ba5ce9464ade31c55571c96abd060
4
- data.tar.gz: c26d4c8262ba89782b429e685d082c38688dea435ecb35cfb861ec7c97173b3b
3
+ metadata.gz: eb41261b9e19c46609f4e7a8867d73b5a902acecc3db3362b4a47e2cacefa9db
4
+ data.tar.gz: ce644b10fa63055065960bb72a61eb53077d4bbe110eac40ed5bd6918d2487c1
5
5
  SHA512:
6
- metadata.gz: 28ad02efc0768a343e259f7beb5828974fc8c818073f46e2f37d36b85e813806f158d66a3d2890aa208ba501b2236c06e863201b4297d3c4266f404cf138eabd
7
- data.tar.gz: c410f3fa9c054560f14578c5618af4735359a793b63ecf3435e33653eeaa7ad13624003d9e00ca874660443ed1eb32bbec63ba2c40ec6a4ca45866b6263dc6e5
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, session_id, event_type, properties, ip = nil, user_agent = nil)
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, session_id: nil, event_type:, properties: {}, ip: nil, user_agent: nil)
11
- TrackRequest.new(user_id, visitor_id, session_id, event_type, properties, ip, user_agent).call
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
@@ -26,9 +26,5 @@ module Mbuzz
26
26
  def mbuzz_visitor_id
27
27
  request.env[ENV_VISITOR_ID_KEY]
28
28
  end
29
-
30
- def mbuzz_session_id
31
- request.env[ENV_SESSION_ID_KEY]
32
- end
33
29
  end
34
30
  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
- RequestContext.with_context(request: request) do
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
- set_cookies(headers, context, request)
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
- session_id: resolve_session_id(request),
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
- def new_session?(request)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbuzz
4
- VERSION = "0.6.8"
4
+ VERSION = "0.7.1"
5
5
  end
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) || fallback_visitor_id
70
+ current_visitor_id || RequestContext.current&.request&.env&.dig(ENV_VISITOR_ID_KEY)
73
71
  end
74
72
 
75
- def self.fallback_visitor_id
76
- @fallback_visitor_id ||= Visitor::Identifier.generate
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
- def self.user_id
81
- RequestContext.current&.request&.env&.dig(ENV_USER_ID_KEY)
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.session_id
85
- RequestContext.current&.request&.env&.dig(ENV_SESSION_ID_KEY)
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
- def self.event(event_type, **properties)
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: visitor_id,
104
- session_id: session_id,
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
- def self.conversion(conversion_type, revenue: nil, user_id: nil, is_acquisition: false, inherit_acquisition: false, **properties)
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: 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
- **Status**: Proposed
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.6.8
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