mbuzz 0.6.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3c9557bce57bad2c91864640fcfff650794bb4de6d56afd3497eaa693247cc10
4
+ data.tar.gz: fef9164f73131538bc7e6df11e2b5fc802a967580b65b26426aa671e5d68cc26
5
+ SHA512:
6
+ metadata.gz: 2c0a78057cf989d08703c758f70b2c6711542c12994662ae47e5b9c67d1462aacd6e80d65b790bb287bb70e6ca03d61c60342eecb16f6c62059f4762359233ad
7
+ data.tar.gz: 8a6782fb3f84ce855438f64066becfb411f46f3db8ae68504dc5a96774b6a9ed4b0a04607da386888fca6683663c2ceddd1bd466b448df2fe82f4900fc03eee5
data/CHANGELOG.md ADDED
@@ -0,0 +1,92 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.6.0] - 2025-12-05
9
+
10
+ ### Added
11
+
12
+ - **Path filtering in middleware** - Automatically skip tracking for:
13
+ - Health check endpoints (`/up`, `/health`, `/healthz`, `/ping`)
14
+ - Asset paths (`/assets`, `/packs`, `/rails/active_storage`)
15
+ - WebSocket paths (`/cable`)
16
+ - API paths (`/api`)
17
+ - Static assets by extension (`.js`, `.css`, `.png`, `.woff2`, etc.)
18
+ - `skip_paths` configuration option - Add custom paths to skip
19
+ - `skip_extensions` configuration option - Add custom extensions to skip
20
+
21
+ ### Fixed
22
+
23
+ - Health check requests no longer create sessions or consume API quota
24
+ - Static asset requests no longer pollute tracking data
25
+
26
+ ### Usage
27
+
28
+ ```ruby
29
+ Mbuzz.init(
30
+ api_key: "sk_live_...",
31
+ skip_paths: ["/admin", "/internal"], # Optional: additional paths to skip
32
+ skip_extensions: [".pdf", ".xml"] # Optional: additional extensions to skip
33
+ )
34
+ ```
35
+
36
+ ## [0.5.0] - 2025-11-29
37
+
38
+ ### Breaking Changes
39
+
40
+ - `Mbuzz.configure` replaced with `Mbuzz.init`
41
+ - `Mbuzz.track` renamed to `Mbuzz.event`
42
+ - `Mbuzz.alias` removed - merged into `Mbuzz.identify`
43
+ - `Mbuzz.identify` signature changed: positional user_id, keyword traits/visitor_id
44
+
45
+ ### Added
46
+
47
+ - `Mbuzz.init(api_key:, ...)` - new configuration method
48
+ - `Mbuzz.event(event_type, **properties)` - cleaner event tracking
49
+ - Automatic visitor linking in `identify` when visitor_id available
50
+ - `enriched_properties` in RequestContext for URL/referrer auto-enrichment
51
+
52
+ ### Removed
53
+
54
+ - `Mbuzz.configure` block syntax
55
+ - `Mbuzz.track` method
56
+ - `Mbuzz.alias` method
57
+ - `Mbuzz::Client::AliasRequest` class
58
+
59
+ ## [0.2.0] - 2025-11-25
60
+
61
+ ### BREAKING CHANGES
62
+
63
+ This release fixes critical bugs that prevented events from being tracked. You MUST update your code to use this version.
64
+
65
+ ### Changed
66
+
67
+ - **BREAKING**: Renamed `event:` parameter to `event_type:` to match backend API
68
+ - Before: `Mbuzz.track(event: 'Signup', user_id: 1)`
69
+ - After: `Mbuzz.track(event_type: 'Signup', user_id: 1)`
70
+ - Migration: Search/replace `event:` → `event_type:` in all `Mbuzz.track()` calls
71
+
72
+ - **BREAKING**: Changed timestamp format from Unix epoch to ISO8601
73
+ - Before: Sent `1732550400` (integer)
74
+ - After: Sends `"2025-11-25T10:30:00Z"` (ISO8601 string)
75
+ - Migration: No action required - gem handles this automatically
76
+
77
+ ### Fixed
78
+
79
+ - Events are now correctly formatted and accepted by backend
80
+ - Timestamps are now in UTC with ISO8601 format
81
+
82
+ ## [0.1.0] - 2025-11-25
83
+
84
+ ### Added
85
+
86
+ - Initial release
87
+ - Event tracking with `Mbuzz::Client.track()`
88
+ - User identification with `Mbuzz::Client.identify()`
89
+ - Visitor aliasing with `Mbuzz::Client.alias()`
90
+ - Automatic visitor and session management via middleware
91
+ - Rails integration via Railtie
92
+ - Controller helpers for convenient tracking
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 TODO: Write your name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # mbuzz
2
+
3
+ Server-side multi-touch attribution for Ruby. Track customer journeys, attribute conversions, know which channels drive revenue.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'mbuzz'
11
+ ```
12
+
13
+ Then:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### 1. Initialize
22
+
23
+ ```ruby
24
+ # config/initializers/mbuzz.rb
25
+ Mbuzz.init(api_key: ENV['MBUZZ_API_KEY'])
26
+ ```
27
+
28
+ ### 2. Track Events
29
+
30
+ Track steps in the customer journey:
31
+
32
+ ```ruby
33
+ Mbuzz.event("page_view", url: request.url)
34
+ Mbuzz.event("add_to_cart", product_id: "SKU-123", price: 49.99)
35
+ Mbuzz.event("checkout_started", cart_total: 99.99)
36
+
37
+ # Group events into funnels for focused analysis
38
+ Mbuzz.event("signup_start", funnel: "signup", source: "homepage")
39
+ Mbuzz.event("signup_complete", funnel: "signup")
40
+ ```
41
+
42
+ ### 3. Track Conversions
43
+
44
+ Record revenue-generating outcomes:
45
+
46
+ ```ruby
47
+ Mbuzz.conversion("purchase",
48
+ revenue: 99.99,
49
+ funnel: "purchase", # Optional: group into funnel
50
+ order_id: order.id
51
+ )
52
+ ```
53
+
54
+ ### 4. Identify Users
55
+
56
+ Link visitors to known users (enables cross-device attribution):
57
+
58
+ ```ruby
59
+ # On signup or login
60
+ Mbuzz.identify(current_user.id,
61
+ traits: {
62
+ email: current_user.email,
63
+ name: current_user.name
64
+ }
65
+ )
66
+ ```
67
+
68
+ ## Funnels
69
+
70
+ Group related events into **funnels** for focused conversion analysis in your dashboard.
71
+
72
+ ```ruby
73
+ # Signup funnel
74
+ Mbuzz.event("pricing_view", funnel: "signup")
75
+ Mbuzz.event("signup_start", funnel: "signup")
76
+ Mbuzz.event("signup_complete", funnel: "signup")
77
+
78
+ # Purchase funnel
79
+ Mbuzz.event("add_to_cart", funnel: "purchase")
80
+ Mbuzz.event("checkout_started", funnel: "purchase")
81
+ Mbuzz.conversion("purchase", funnel: "purchase", revenue: 99.99)
82
+ ```
83
+
84
+ **Why use funnels?**
85
+ - Separate signup flow from purchase flow
86
+ - Analyze each conversion path independently
87
+ - Filter dashboard to specific customer journeys
88
+
89
+ ## Rails Integration
90
+
91
+ mbuzz auto-integrates with Rails via Railtie:
92
+ - Middleware for visitor and session cookie management
93
+ - Controller helpers for convenient access
94
+
95
+ ### Controller Helpers
96
+
97
+ ```ruby
98
+ class ApplicationController < ActionController::Base
99
+ # Access current IDs
100
+ mbuzz_visitor_id # Returns the visitor ID from cookie
101
+ mbuzz_user_id # Returns user_id from session["user_id"]
102
+ mbuzz_session_id # Returns the session ID from cookie
103
+ end
104
+ ```
105
+
106
+ **Note:** For tracking and identification, prefer the main API which auto-enriches events with URL/referrer:
107
+
108
+ ```ruby
109
+ # Recommended - use main API in controllers
110
+ Mbuzz.event("page_view", page: request.path)
111
+ Mbuzz.identify(current_user.id, traits: { email: current_user.email })
112
+ ```
113
+
114
+ ### Context Accessors
115
+
116
+ Access IDs from anywhere in your request cycle:
117
+
118
+ ```ruby
119
+ Mbuzz.visitor_id # Current visitor ID (from cookie)
120
+ Mbuzz.user_id # Current user ID (from session["user_id"])
121
+ Mbuzz.session_id # Current session ID (from cookie)
122
+ ```
123
+
124
+ ## Rack / Sinatra Integration
125
+
126
+ For non-Rails apps, add the middleware manually:
127
+
128
+ ```ruby
129
+ # config.ru or app.rb
130
+ require 'mbuzz'
131
+
132
+ Mbuzz.init(api_key: ENV['MBUZZ_API_KEY'])
133
+
134
+ use Mbuzz::Middleware::Tracking
135
+ run MyApp
136
+ ```
137
+
138
+ ## Configuration Options
139
+
140
+ ```ruby
141
+ Mbuzz.init(
142
+ api_key: "sk_live_...", # Required - from mbuzz.co dashboard
143
+ api_url: "https://mbuzz.co/api/v1", # Optional - API endpoint
144
+ debug: false # Optional - enable debug logging
145
+ )
146
+ ```
147
+
148
+ ## The 4-Call Model
149
+
150
+ | Method | When to Use |
151
+ |--------|-------------|
152
+ | `init` | Once on app boot |
153
+ | `event` | User interactions, funnel steps |
154
+ | `conversion` | Purchases, signups, any revenue event |
155
+ | `identify` | Login, signup, when you know the user |
156
+
157
+ ## Error Handling
158
+
159
+ mbuzz never raises exceptions. All methods return `false` on failure and log errors in debug mode.
160
+
161
+ ## Requirements
162
+
163
+ - Ruby 3.0+
164
+ - Rails 6.0+ (for automatic integration) or any Rack app
165
+
166
+ ## Links
167
+
168
+ - [Documentation](https://mbuzz.co/docs)
169
+ - [Dashboard](https://mbuzz.co/dashboard)
170
+
171
+ ## License
172
+
173
+ MIT License
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
data/lib/mbuzz/api.rb ADDED
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+ require "openssl"
7
+
8
+ module Mbuzz
9
+ class Api
10
+ def self.post(path, payload)
11
+ return false unless enabled_and_configured?
12
+
13
+ response = http_client(path).request(build_request(path, payload))
14
+ success?(response)
15
+ rescue ConfigurationError, Net::ReadTimeout, Net::OpenTimeout, Net::HTTPError => e
16
+ log_error("#{e.class}: #{e.message}")
17
+ false
18
+ end
19
+
20
+ def self.post_with_response(path, payload)
21
+ return nil unless enabled_and_configured?
22
+
23
+ response = http_client(path).request(build_request(path, payload))
24
+ return nil unless success?(response)
25
+
26
+ JSON.parse(response.body)
27
+ rescue ConfigurationError, Net::ReadTimeout, Net::OpenTimeout, Net::HTTPError, JSON::ParserError => e
28
+ log_error("#{e.class}: #{e.message}")
29
+ nil
30
+ end
31
+
32
+ def self.enabled_and_configured?
33
+ return false unless config.enabled
34
+ config.validate!
35
+ true
36
+ rescue ConfigurationError => e
37
+ log_error(e.message)
38
+ false
39
+ end
40
+ private_class_method :enabled_and_configured?
41
+
42
+ def self.uri(path)
43
+ @uri_cache ||= {}
44
+ @uri_cache[path] ||= begin
45
+ base = config.api_url.chomp("/")
46
+ endpoint = path.start_with?("/") ? path : "/#{path}"
47
+ URI.parse("#{base}#{endpoint}")
48
+ end
49
+ end
50
+ private_class_method :uri
51
+
52
+ def self.http_client(path)
53
+ Net::HTTP.new(uri(path).host, uri(path).port).tap do |http|
54
+ if uri(path).scheme == "https"
55
+ http.use_ssl = true
56
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
57
+ http.cert_store = ssl_cert_store
58
+ end
59
+ http.open_timeout = config.timeout
60
+ http.read_timeout = config.timeout
61
+ end
62
+ end
63
+ private_class_method :http_client
64
+
65
+ def self.ssl_cert_store
66
+ @ssl_cert_store ||= OpenSSL::X509::Store.new.tap do |store|
67
+ store.set_default_paths
68
+ # Don't set any CRL flags - Let's Encrypt uses OCSP, not CRL
69
+ store.flags = 0
70
+ end
71
+ end
72
+ private_class_method :ssl_cert_store
73
+
74
+ def self.build_request(path, payload)
75
+ Net::HTTP::Post.new(uri(path).path).tap do |request|
76
+ request["Authorization"] = "Bearer #{config.api_key}"
77
+ request["Content-Type"] = "application/json"
78
+ request["User-Agent"] = "mbuzz-ruby/#{VERSION}"
79
+ request.body = JSON.generate(payload)
80
+ end
81
+ end
82
+ private_class_method :build_request
83
+
84
+ def self.success?(response)
85
+ return true if response.code.to_i.between?(200, 299)
86
+
87
+ log_error("API #{response.code}: #{response.body}")
88
+ false
89
+ end
90
+ private_class_method :success?
91
+
92
+ def self.log_error(message)
93
+ warn "[mbuzz] #{message}" if config.debug
94
+ end
95
+ private_class_method :log_error
96
+
97
+ def self.config
98
+ Mbuzz.config
99
+ end
100
+ private_class_method :config
101
+ end
102
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbuzz
4
+ class Client
5
+ class ConversionRequest
6
+ def initialize(event_id:, visitor_id:, user_id:, conversion_type:, revenue:, currency:, is_acquisition:, inherit_acquisition:, properties:)
7
+ @event_id = event_id
8
+ @visitor_id = visitor_id
9
+ @user_id = user_id
10
+ @conversion_type = conversion_type
11
+ @revenue = revenue
12
+ @currency = currency
13
+ @is_acquisition = is_acquisition
14
+ @inherit_acquisition = inherit_acquisition
15
+ @properties = properties
16
+ end
17
+
18
+ def call
19
+ return false unless valid?
20
+
21
+ { success: true, conversion_id: conversion_id, attribution: attribution }
22
+ end
23
+
24
+ private
25
+
26
+ def valid?
27
+ has_identifier? && present?(@conversion_type) && hash?(@properties) && conversion_id
28
+ end
29
+
30
+ def has_identifier?
31
+ present?(@event_id) || present?(@visitor_id) || present?(@user_id)
32
+ end
33
+
34
+ def conversion_id
35
+ @conversion_id ||= response&.dig("conversion", "id")
36
+ end
37
+
38
+ def attribution
39
+ response&.dig("attribution")
40
+ end
41
+
42
+ def response
43
+ @response ||= Api.post_with_response(CONVERSIONS_PATH, payload)
44
+ end
45
+
46
+ def payload
47
+ { conversion: conversion_payload }
48
+ end
49
+
50
+ def conversion_payload
51
+ base_payload
52
+ .merge(optional_identifiers)
53
+ .merge(optional_acquisition_fields)
54
+ end
55
+
56
+ def base_payload
57
+ {
58
+ conversion_type: @conversion_type,
59
+ currency: @currency,
60
+ properties: @properties,
61
+ timestamp: Time.now.utc.iso8601
62
+ }
63
+ end
64
+
65
+ def optional_identifiers
66
+ {}.tap do |h|
67
+ h[:event_id] = @event_id if @event_id
68
+ h[:visitor_id] = @visitor_id if @visitor_id
69
+ h[:user_id] = @user_id if @user_id
70
+ h[:revenue] = @revenue if @revenue
71
+ end
72
+ end
73
+
74
+ def optional_acquisition_fields
75
+ {}.tap do |h|
76
+ h[:is_acquisition] = @is_acquisition if @is_acquisition
77
+ h[:inherit_acquisition] = @inherit_acquisition if @inherit_acquisition
78
+ end
79
+ end
80
+
81
+ def present?(value) = value && !value.to_s.strip.empty?
82
+ def hash?(value) = value.is_a?(Hash)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbuzz
4
+ class Client
5
+ class IdentifyRequest
6
+ def initialize(user_id, visitor_id, traits)
7
+ @user_id = user_id
8
+ @visitor_id = visitor_id
9
+ @traits = traits
10
+ end
11
+
12
+ def call
13
+ return false unless valid?
14
+
15
+ Api.post(IDENTIFY_PATH, payload)
16
+ end
17
+
18
+ private
19
+
20
+ def valid?
21
+ id?(@user_id) && hash?(@traits)
22
+ end
23
+
24
+ def payload
25
+ {
26
+ user_id: @user_id,
27
+ visitor_id: @visitor_id,
28
+ traits: @traits,
29
+ timestamp: Time.now.utc.iso8601
30
+ }.compact
31
+ end
32
+
33
+ def id?(value) = value.is_a?(String) || value.is_a?(Numeric)
34
+ def hash?(value) = value.is_a?(Hash)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,43 @@
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
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbuzz
4
+ class Client
5
+ class TrackRequest
6
+ def initialize(user_id, visitor_id, session_id, event_type, properties)
7
+ @user_id = user_id
8
+ @visitor_id = visitor_id
9
+ @session_id = session_id
10
+ @event_type = event_type
11
+ @properties = properties
12
+ end
13
+
14
+ def call
15
+ return false unless valid?
16
+
17
+ { success: true, event_id: event["id"], event_type: event["event_type"],
18
+ visitor_id: event["visitor_id"], session_id: event["session_id"] }
19
+ end
20
+
21
+ private
22
+
23
+ def valid?
24
+ present?(@event_type) && hash?(@properties) && (@user_id || @visitor_id) && event
25
+ end
26
+
27
+ def event
28
+ @event ||= response&.dig("events", 0)
29
+ end
30
+
31
+ def response
32
+ @response ||= Api.post_with_response(EVENTS_PATH, { events: [payload] })
33
+ end
34
+
35
+ def payload
36
+ {
37
+ user_id: @user_id,
38
+ visitor_id: @visitor_id,
39
+ session_id: @session_id,
40
+ event_type: @event_type,
41
+ properties: @properties,
42
+ timestamp: Time.now.utc.iso8601
43
+ }.compact
44
+ end
45
+
46
+ def present?(value) = value && !value.to_s.strip.empty?
47
+ def hash?(value) = value.is_a?(Hash)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client/track_request"
4
+ require_relative "client/identify_request"
5
+ require_relative "client/conversion_request"
6
+ require_relative "client/session_request"
7
+
8
+ module Mbuzz
9
+ class Client
10
+ def self.track(user_id: nil, visitor_id: nil, session_id: nil, event_type:, properties: {})
11
+ TrackRequest.new(user_id, visitor_id, session_id, event_type, properties).call
12
+ end
13
+
14
+ def self.identify(user_id:, visitor_id: nil, traits: {})
15
+ IdentifyRequest.new(user_id, visitor_id, traits).call
16
+ end
17
+
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: {})
19
+ ConversionRequest.new(
20
+ event_id: event_id,
21
+ visitor_id: visitor_id,
22
+ user_id: user_id,
23
+ conversion_type: conversion_type,
24
+ revenue: revenue,
25
+ currency: currency,
26
+ is_acquisition: is_acquisition,
27
+ inherit_acquisition: inherit_acquisition,
28
+ properties: properties
29
+ ).call
30
+ 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
+ end
36
+ end