mbuzz 0.6.8 → 0.7.0

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: 0fa7c025330829c88ab9a75a22ca286d14adc1df7872295c5519df4c89536db9
4
+ data.tar.gz: 62d6675e5ffe9504e2262f84551c286366acc2ad42b6ca6d224367902d9ec3fb
5
5
  SHA512:
6
- metadata.gz: 28ad02efc0768a343e259f7beb5828974fc8c818073f46e2f37d36b85e813806f158d66a3d2890aa208ba501b2236c06e863201b4297d3c4266f404cf138eabd
7
- data.tar.gz: c410f3fa9c054560f14578c5618af4735359a793b63ecf3435e33653eeaa7ad13624003d9e00ca874660443ed1eb32bbec63ba2c40ec6a4ca45866b6263dc6e5
6
+ metadata.gz: eb90968b0d4c17bdc7c5019fa0058c257233bc00e1921f0b70a3dbf885fd8aecd1f9d80ab125d31ab535be5e55a44ecc2e0d46e9cb9c51fef4b9305634241b2f
7
+ data.tar.gz: cef7cbbc7817a6d7c4eca635594ac9e3b9661d81b33cc7d956376cb9fd763a6d216651e6ec499146e31dca042f024216f3c85c08c6d83f4f31dfb1fff64e8b56
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
@@ -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
@@ -17,13 +17,10 @@ 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
21
  RequestContext.with_context(request: request) do
23
- create_session_if_new(context, request)
24
-
25
22
  status, headers, body = @app.call(env)
26
- set_cookies(headers, context, request)
23
+ set_visitor_cookie(headers, context, request)
27
24
  [status, headers, body]
28
25
  end
29
26
  end
@@ -51,9 +48,7 @@ module Mbuzz
51
48
  def build_request_context(request)
52
49
  {
53
50
  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)
51
+ user_id: user_id_from_session(request)
57
52
  }.freeze
58
53
  end
59
54
 
@@ -61,91 +56,15 @@ module Mbuzz
61
56
  visitor_id_from_cookie(request) || Visitor::Identifier.generate
62
57
  end
63
58
 
64
- def resolve_session_id(request)
65
- session_id_from_cookie(request) || generate_session_id(request)
66
- end
67
-
68
59
  def visitor_id_from_cookie(request)
69
60
  request.cookies[VISITOR_COOKIE_NAME]
70
61
  end
71
62
 
72
- def session_id_from_cookie(request)
73
- request.cookies[SESSION_COOKIE_NAME]
74
- end
75
-
76
63
  def user_id_from_session(request)
77
64
  request.session[SESSION_USER_ID_KEY] if request.session
78
65
  end
79
66
 
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
67
+ # Cookie setting - only visitor cookie (sessions are server-side)
149
68
 
150
69
  def set_visitor_cookie(headers, context, request)
151
70
  Rack::Utils.set_cookie_header!(
@@ -155,14 +74,6 @@ module Mbuzz
155
74
  )
156
75
  end
157
76
 
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
77
  def visitor_cookie_options(context, request)
167
78
  base_cookie_options(request).merge(
168
79
  value: context[:visitor_id],
@@ -170,13 +81,6 @@ module Mbuzz
170
81
  )
171
82
  end
172
83
 
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
84
  def base_cookie_options(request)
181
85
  options = {
182
86
  path: VISITOR_COOKIE_PATH,
data/lib/mbuzz/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbuzz
4
- VERSION = "0.6.8"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/mbuzz.rb CHANGED
@@ -3,7 +3,6 @@
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"
@@ -18,20 +17,15 @@ module Mbuzz
18
17
  EVENTS_PATH = "/events"
19
18
  IDENTIFY_PATH = "/identify"
20
19
  CONVERSIONS_PATH = "/conversions"
21
- SESSIONS_PATH = "/sessions"
22
20
 
23
21
  VISITOR_COOKIE_NAME = "_mbuzz_vid"
24
22
  VISITOR_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 2 # 2 years
25
23
  VISITOR_COOKIE_PATH = "/"
26
24
  VISITOR_COOKIE_SAME_SITE = "Lax"
27
25
 
28
- SESSION_COOKIE_NAME = "_mbuzz_sid"
29
- SESSION_COOKIE_MAX_AGE = 30 * 60 # 30 minutes
30
-
31
26
  SESSION_USER_ID_KEY = "user_id"
32
27
  ENV_USER_ID_KEY = "mbuzz.user_id"
33
28
  ENV_VISITOR_ID_KEY = "mbuzz.visitor_id"
34
- ENV_SESSION_ID_KEY = "mbuzz.session_id"
35
29
 
36
30
  # ============================================================================
37
31
  # Configuration
@@ -81,10 +75,6 @@ module Mbuzz
81
75
  RequestContext.current&.request&.env&.dig(ENV_USER_ID_KEY)
82
76
  end
83
77
 
84
- def self.session_id
85
- RequestContext.current&.request&.env&.dig(ENV_SESSION_ID_KEY)
86
- end
87
-
88
78
  # ============================================================================
89
79
  # 4-Call Model API
90
80
  # ============================================================================
@@ -93,20 +83,24 @@ module Mbuzz
93
83
  #
94
84
  # @param event_type [String] The name of the event
95
85
  # @param properties [Hash] Custom event properties (url, referrer auto-added)
86
+ # @param identifier [Hash, nil] Optional identifier for cross-device identity resolution
96
87
  # @return [Hash, false] Result hash on success, false on failure
97
88
  #
98
89
  # @example
99
90
  # Mbuzz.event("add_to_cart", product_id: "SKU-123", price: 49.99)
100
91
  #
101
- def self.event(event_type, **properties)
92
+ # @example With identifier for cross-device tracking
93
+ # Mbuzz.event("page_view", identifier: { email: "user@example.com" })
94
+ #
95
+ def self.event(event_type, identifier: nil, **properties)
102
96
  Client.track(
103
97
  visitor_id: visitor_id,
104
- session_id: session_id,
105
98
  user_id: user_id,
106
99
  event_type: event_type,
107
100
  properties: enriched_properties(properties),
108
101
  ip: current_ip,
109
- user_agent: current_user_agent
102
+ user_agent: current_user_agent,
103
+ identifier: identifier
110
104
  )
111
105
  end
112
106
 
@@ -123,6 +117,7 @@ module Mbuzz
123
117
  # @param user_id [String, nil] User ID for acquisition-linked conversions
124
118
  # @param is_acquisition [Boolean] Mark this as the acquisition conversion for this user
125
119
  # @param inherit_acquisition [Boolean] Inherit attribution from user's acquisition conversion
120
+ # @param identifier [Hash, nil] Optional identifier for cross-device identity resolution
126
121
  # @param properties [Hash] Custom properties
127
122
  # @return [Hash, false] Result hash on success, false on failure
128
123
  #
@@ -135,7 +130,10 @@ module Mbuzz
135
130
  # @example Recurring revenue (inherits attribution from acquisition)
136
131
  # Mbuzz.conversion("payment", user_id: "user_123", revenue: 49.00, inherit_acquisition: true)
137
132
  #
138
- def self.conversion(conversion_type, revenue: nil, user_id: nil, is_acquisition: false, inherit_acquisition: false, **properties)
133
+ # @example With identifier for cross-device tracking
134
+ # Mbuzz.conversion("purchase", identifier: { email: "user@example.com" })
135
+ #
136
+ def self.conversion(conversion_type, revenue: nil, user_id: nil, is_acquisition: false, inherit_acquisition: false, identifier: nil, **properties)
139
137
  Client.conversion(
140
138
  visitor_id: visitor_id,
141
139
  user_id: user_id,
@@ -143,7 +141,10 @@ module Mbuzz
143
141
  revenue: revenue,
144
142
  is_acquisition: is_acquisition,
145
143
  inherit_acquisition: inherit_acquisition,
146
- properties: enriched_properties(properties)
144
+ properties: enriched_properties(properties),
145
+ ip: current_ip,
146
+ user_agent: current_user_agent,
147
+ identifier: identifier
147
148
  )
148
149
  end
149
150
 
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mbuzz team
@@ -44,14 +44,12 @@ 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
51
50
  - lib/mbuzz/middleware/tracking.rb
52
51
  - lib/mbuzz/railtie.rb
53
52
  - lib/mbuzz/request_context.rb
54
- - lib/mbuzz/session/id_generator.rb
55
53
  - lib/mbuzz/version.rb
56
54
  - lib/mbuzz/visitor/identifier.rb
57
55
  - 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