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 +7 -0
- data/CHANGELOG.md +92 -0
- data/LICENSE.txt +21 -0
- data/README.md +173 -0
- data/Rakefile +8 -0
- data/lib/mbuzz/api.rb +102 -0
- data/lib/mbuzz/client/conversion_request.rb +85 -0
- data/lib/mbuzz/client/identify_request.rb +37 -0
- data/lib/mbuzz/client/session_request.rb +43 -0
- data/lib/mbuzz/client/track_request.rb +50 -0
- data/lib/mbuzz/client.rb +36 -0
- data/lib/mbuzz/configuration.rb +51 -0
- data/lib/mbuzz/controller_helpers.rb +34 -0
- data/lib/mbuzz/middleware/tracking.rb +157 -0
- data/lib/mbuzz/railtie.rb +13 -0
- data/lib/mbuzz/request_context.rb +40 -0
- data/lib/mbuzz/version.rb +5 -0
- data/lib/mbuzz/visitor/identifier.rb +13 -0
- data/lib/mbuzz.rb +178 -0
- data/lib/specs/old/SPECIFICATION.md +695 -0
- data/lib/specs/old/conversions.md +585 -0
- data/lib/specs/old/event_ids_response.md +346 -0
- data/lib/specs/old/v0.2.0_breaking_changes.md +519 -0
- data/lib/specs/old/v2.0.0_sessions_upgrade.md +265 -0
- data/lib/specs/v0.5.0_four_call_model.md +505 -0
- data/mbuzz-0.6.0.gem +0 -0
- data/sig/mbuzz.rbs +4 -0
- metadata +89 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
# Conversions API - Technical Specification
|
|
2
|
+
|
|
3
|
+
**Version**: 1.1.0
|
|
4
|
+
**Created**: 2025-11-26
|
|
5
|
+
**Updated**: 2025-11-26
|
|
6
|
+
**Status**: Ready for Implementation
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Add conversion tracking to the mbuzz Ruby gem. Conversions are the critical attribution endpoint - they trigger the attribution calculation that credits marketing touchpoints for revenue.
|
|
13
|
+
|
|
14
|
+
**Purpose**: Allow Ruby SDK users to track conversions (purchases, signups, upgrades) and retrieve attribution data showing which marketing channels drove the conversion.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## API Design
|
|
19
|
+
|
|
20
|
+
### Mbuzz::Client.conversion
|
|
21
|
+
|
|
22
|
+
Track a conversion event and trigger attribution calculation.
|
|
23
|
+
|
|
24
|
+
**Method Signature**:
|
|
25
|
+
```ruby
|
|
26
|
+
Mbuzz::Client.conversion(
|
|
27
|
+
event_id: nil, # Identifier option A - Link to specific event
|
|
28
|
+
visitor_id: nil, # Identifier option B - Visitor ID (uses most recent session)
|
|
29
|
+
conversion_type:, # Required - Type: "purchase", "signup", "upgrade", etc.
|
|
30
|
+
revenue: nil, # Optional - Revenue amount (numeric)
|
|
31
|
+
currency: "USD", # Optional - Currency code (default: USD)
|
|
32
|
+
properties: {} # Optional - Additional metadata
|
|
33
|
+
)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Parameters**:
|
|
37
|
+
| Parameter | Type | Required | Description |
|
|
38
|
+
|-----------|------|----------|-------------|
|
|
39
|
+
| `event_id` | String | One of event_id/visitor_id | Prefixed event ID (`evt_*`) - visitor/session derived from event |
|
|
40
|
+
| `visitor_id` | String | One of event_id/visitor_id | Raw visitor ID (64-char hex from `_mbuzz_vid`) - uses most recent session |
|
|
41
|
+
| `conversion_type` | String | Yes | Type of conversion (e.g., "purchase", "signup") |
|
|
42
|
+
| `revenue` | Numeric | No | Revenue amount for ROI calculations |
|
|
43
|
+
| `currency` | String | No | ISO 4217 currency code (default: "USD") |
|
|
44
|
+
| `properties` | Hash | No | Additional conversion metadata |
|
|
45
|
+
|
|
46
|
+
**Identifier Resolution**:
|
|
47
|
+
- If `event_id` provided: Use event's visitor and session (most precise)
|
|
48
|
+
- If only `visitor_id` provided: Look up visitor, use most recent session
|
|
49
|
+
- If both provided: `event_id` takes precedence
|
|
50
|
+
- If neither provided: Return `false` (validation error)
|
|
51
|
+
|
|
52
|
+
**Returns**:
|
|
53
|
+
- Success: `{ success: true, conversion_id: "conv_abc123", attribution: {...} }`
|
|
54
|
+
- Failure: `false`
|
|
55
|
+
|
|
56
|
+
**Example Usage**:
|
|
57
|
+
```ruby
|
|
58
|
+
# Option A: Event-based (recommended for precise attribution)
|
|
59
|
+
track_result = Mbuzz::Client.track(
|
|
60
|
+
visitor_id: mbuzz_visitor_id,
|
|
61
|
+
event_type: 'checkout_completed',
|
|
62
|
+
properties: { order_id: order.id }
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if track_result[:success]
|
|
66
|
+
result = Mbuzz::Client.conversion(
|
|
67
|
+
event_id: track_result[:event_id],
|
|
68
|
+
conversion_type: 'purchase',
|
|
69
|
+
revenue: order.total
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Option B: Visitor-based (simpler, for direct conversions)
|
|
74
|
+
result = Mbuzz::Client.conversion(
|
|
75
|
+
visitor_id: mbuzz_visitor_id,
|
|
76
|
+
conversion_type: 'purchase',
|
|
77
|
+
revenue: 99.00,
|
|
78
|
+
properties: { plan: 'pro' }
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Access attribution data
|
|
82
|
+
if result[:success]
|
|
83
|
+
puts "Conversion ID: #{result[:conversion_id]}"
|
|
84
|
+
|
|
85
|
+
result[:attribution][:models].each do |model_name, credits|
|
|
86
|
+
puts "#{model_name}:"
|
|
87
|
+
credits.each do |credit|
|
|
88
|
+
puts " #{credit[:channel]}: #{(credit[:credit] * 100).round(1)}%"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## When to Use Each Approach
|
|
97
|
+
|
|
98
|
+
| Approach | Use Case |
|
|
99
|
+
|----------|----------|
|
|
100
|
+
| **Event-based** (`event_id`) | Tie conversion to specific action (checkout button click, form submit) |
|
|
101
|
+
| **Visitor-based** (`visitor_id`) | Direct conversions, offline imports, webhook integrations, simpler SDK usage |
|
|
102
|
+
|
|
103
|
+
**Event-based advantages**:
|
|
104
|
+
- Most precise - conversion tied to exact moment in journey
|
|
105
|
+
- Event has full context (properties, timestamp)
|
|
106
|
+
- Better for debugging and analysis
|
|
107
|
+
|
|
108
|
+
**Visitor-based advantages**:
|
|
109
|
+
- Simpler - don't need to track an event first
|
|
110
|
+
- Works for offline/imported conversions
|
|
111
|
+
- Better for direct purchase flows (land → buy immediately)
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Backend Integration
|
|
116
|
+
|
|
117
|
+
### API Endpoint
|
|
118
|
+
|
|
119
|
+
**POST https://mbuzz.co/api/v1/conversions**
|
|
120
|
+
|
|
121
|
+
**Request Headers**:
|
|
122
|
+
```
|
|
123
|
+
Authorization: Bearer sk_live_abc123...
|
|
124
|
+
Content-Type: application/json
|
|
125
|
+
User-Agent: mbuzz-ruby/1.0.0
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Request Body (Event-based)**:
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"conversion": {
|
|
132
|
+
"event_id": "evt_abc123def456",
|
|
133
|
+
"conversion_type": "purchase",
|
|
134
|
+
"revenue": 99.00,
|
|
135
|
+
"properties": {
|
|
136
|
+
"plan": "pro"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Request Body (Visitor-based)**:
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"conversion": {
|
|
146
|
+
"visitor_id": "65dabef8d611f332d5bb88f5d6870c733d89f962594575b66f0e1de1ede1ebf0",
|
|
147
|
+
"conversion_type": "purchase",
|
|
148
|
+
"revenue": 99.00,
|
|
149
|
+
"properties": {
|
|
150
|
+
"plan": "pro"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Success Response** (201 Created):
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"id": "conv_xyz789",
|
|
160
|
+
"visitor_id": "vis_abc123",
|
|
161
|
+
"conversion_type": "purchase",
|
|
162
|
+
"revenue": "99.0",
|
|
163
|
+
"converted_at": "2025-11-26T10:30:00Z",
|
|
164
|
+
"attribution": {
|
|
165
|
+
"lookback_days": 30,
|
|
166
|
+
"sessions_analyzed": 3,
|
|
167
|
+
"models": {
|
|
168
|
+
"first_touch": [
|
|
169
|
+
{
|
|
170
|
+
"session_id": "sess_111",
|
|
171
|
+
"channel": "organic_search",
|
|
172
|
+
"credit": 1.0,
|
|
173
|
+
"revenue_credit": "99.0"
|
|
174
|
+
}
|
|
175
|
+
],
|
|
176
|
+
"last_touch": [
|
|
177
|
+
{
|
|
178
|
+
"session_id": "sess_333",
|
|
179
|
+
"channel": "email",
|
|
180
|
+
"credit": 1.0,
|
|
181
|
+
"revenue_credit": "99.0"
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
"linear": [
|
|
185
|
+
{
|
|
186
|
+
"session_id": "sess_111",
|
|
187
|
+
"channel": "organic_search",
|
|
188
|
+
"credit": 0.333,
|
|
189
|
+
"revenue_credit": "33.0"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"session_id": "sess_222",
|
|
193
|
+
"channel": "paid_social",
|
|
194
|
+
"credit": 0.333,
|
|
195
|
+
"revenue_credit": "33.0"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"session_id": "sess_333",
|
|
199
|
+
"channel": "email",
|
|
200
|
+
"credit": 0.334,
|
|
201
|
+
"revenue_credit": "33.0"
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Error Response** (422 Unprocessable Entity):
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"errors": ["event_id or visitor_id is required"]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"errors": ["Visitor not found"]
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Attribution Models
|
|
225
|
+
|
|
226
|
+
The backend calculates attribution across all active models for the account:
|
|
227
|
+
|
|
228
|
+
### Implemented Models
|
|
229
|
+
|
|
230
|
+
| Model | Description | Credit Distribution |
|
|
231
|
+
|-------|-------------|---------------------|
|
|
232
|
+
| `first_touch` | First session gets all credit | 100% to first session |
|
|
233
|
+
| `last_touch` | Last session gets all credit | 100% to last session |
|
|
234
|
+
| `linear` | Equal credit to all sessions | Even split across sessions |
|
|
235
|
+
|
|
236
|
+
### Future Models (Not Yet Implemented)
|
|
237
|
+
|
|
238
|
+
| Model | Description |
|
|
239
|
+
|-------|-------------|
|
|
240
|
+
| `time_decay` | More recent sessions get more credit |
|
|
241
|
+
| `u_shaped` | First and last get 40% each, middle splits 20% |
|
|
242
|
+
| `w_shaped` | First, lead creation, last get 30% each, rest splits 10% |
|
|
243
|
+
| `participation` | All sessions get 100% (over-counts to show participation) |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Implementation Details
|
|
248
|
+
|
|
249
|
+
### Client Implementation
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# lib/mbuzz/client.rb
|
|
253
|
+
|
|
254
|
+
def self.conversion(event_id: nil, visitor_id: nil, conversion_type:, revenue: nil, currency: "USD", properties: {})
|
|
255
|
+
ConversionRequest.new(event_id, visitor_id, conversion_type, revenue, currency, properties).call
|
|
256
|
+
end
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### ConversionRequest Implementation
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# lib/mbuzz/client/conversion_request.rb
|
|
263
|
+
|
|
264
|
+
module Mbuzz
|
|
265
|
+
class Client
|
|
266
|
+
class ConversionRequest
|
|
267
|
+
def initialize(event_id, visitor_id, conversion_type, revenue, currency, properties)
|
|
268
|
+
@event_id = event_id
|
|
269
|
+
@visitor_id = visitor_id
|
|
270
|
+
@conversion_type = conversion_type
|
|
271
|
+
@revenue = revenue
|
|
272
|
+
@currency = currency
|
|
273
|
+
@properties = properties
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def call
|
|
277
|
+
return false unless valid?
|
|
278
|
+
|
|
279
|
+
response = Api.post_with_response(CONVERSIONS_PATH, payload)
|
|
280
|
+
return false unless response
|
|
281
|
+
|
|
282
|
+
{
|
|
283
|
+
success: true,
|
|
284
|
+
conversion_id: response["id"],
|
|
285
|
+
attribution: symbolize_attribution(response["attribution"])
|
|
286
|
+
}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
def valid?
|
|
292
|
+
has_identifier? && present?(@conversion_type) && hash?(@properties)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def has_identifier?
|
|
296
|
+
present?(@event_id) || present?(@visitor_id)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def payload
|
|
300
|
+
{
|
|
301
|
+
conversion: base_payload
|
|
302
|
+
.tap { |p| p[:event_id] = @event_id if @event_id }
|
|
303
|
+
.tap { |p| p[:visitor_id] = @visitor_id if @visitor_id }
|
|
304
|
+
.tap { |p| p[:revenue] = @revenue if @revenue }
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def base_payload
|
|
309
|
+
{
|
|
310
|
+
conversion_type: @conversion_type,
|
|
311
|
+
currency: @currency,
|
|
312
|
+
properties: @properties,
|
|
313
|
+
timestamp: Time.now.utc.iso8601
|
|
314
|
+
}
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def symbolize_attribution(attr)
|
|
318
|
+
return nil unless attr
|
|
319
|
+
# Convert string keys to symbols for Ruby-friendly access
|
|
320
|
+
attr.transform_keys(&:to_sym)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def present?(value) = value && !value.to_s.strip.empty?
|
|
324
|
+
def hash?(value) = value.is_a?(Hash)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Constants
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
# lib/mbuzz.rb
|
|
334
|
+
|
|
335
|
+
CONVERSIONS_PATH = "/conversions"
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Validation Rules
|
|
341
|
+
|
|
342
|
+
### Client-Side Validation
|
|
343
|
+
|
|
344
|
+
| Field | Rule | Error Behavior |
|
|
345
|
+
|-------|------|----------------|
|
|
346
|
+
| `event_id` OR `visitor_id` | At least one required | Return `false` |
|
|
347
|
+
| `conversion_type` | Required, non-empty string | Return `false` |
|
|
348
|
+
| `revenue` | Optional, numeric if present | Return `false` if non-numeric |
|
|
349
|
+
| `properties` | Optional, must be Hash | Return `false` if not Hash |
|
|
350
|
+
|
|
351
|
+
### Backend Validation
|
|
352
|
+
|
|
353
|
+
| Field | Rule | Error Response |
|
|
354
|
+
|-------|------|----------------|
|
|
355
|
+
| `event_id` OR `visitor_id` | At least one required | 422 "event_id or visitor_id is required" |
|
|
356
|
+
| `event_id` | Must exist if provided | 422 "Event not found" |
|
|
357
|
+
| `visitor_id` | Must exist if provided | 422 "Visitor not found" |
|
|
358
|
+
| `conversion_type` | Non-empty string | 422 "conversion_type is required" |
|
|
359
|
+
| `revenue` | Numeric if present | 422 "Revenue must be numeric" |
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Error Handling
|
|
364
|
+
|
|
365
|
+
Following the gem's philosophy: **never raise exceptions**.
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# All errors return false
|
|
369
|
+
result = Mbuzz::Client.conversion(conversion_type: "purchase")
|
|
370
|
+
# => false (no identifier)
|
|
371
|
+
|
|
372
|
+
result = Mbuzz::Client.conversion(visitor_id: "abc", conversion_type: "")
|
|
373
|
+
# => false (empty conversion_type)
|
|
374
|
+
|
|
375
|
+
# Network errors return false
|
|
376
|
+
result = Mbuzz::Client.conversion(visitor_id: "abc", conversion_type: "purchase")
|
|
377
|
+
# => false (if network timeout)
|
|
378
|
+
|
|
379
|
+
# Success returns hash with data
|
|
380
|
+
result = Mbuzz::Client.conversion(visitor_id: "abc", conversion_type: "purchase")
|
|
381
|
+
# => { success: true, conversion_id: "conv_xyz", attribution: {...} }
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Testing
|
|
387
|
+
|
|
388
|
+
### Unit Tests
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
# test/mbuzz/client/conversion_request_test.rb
|
|
392
|
+
|
|
393
|
+
class ConversionRequestTest < Minitest::Test
|
|
394
|
+
# Identifier validation
|
|
395
|
+
def test_requires_event_id_or_visitor_id
|
|
396
|
+
result = Mbuzz::Client.conversion(
|
|
397
|
+
conversion_type: "purchase"
|
|
398
|
+
)
|
|
399
|
+
assert_equal false, result
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def test_accepts_event_id_only
|
|
403
|
+
stub_successful_conversion_response
|
|
404
|
+
|
|
405
|
+
result = Mbuzz::Client.conversion(
|
|
406
|
+
event_id: "evt_abc123",
|
|
407
|
+
conversion_type: "purchase"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
assert result[:success]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def test_accepts_visitor_id_only
|
|
414
|
+
stub_successful_conversion_response
|
|
415
|
+
|
|
416
|
+
result = Mbuzz::Client.conversion(
|
|
417
|
+
visitor_id: "65dabef8d611f332d5bb88f5d6870c733d89f962594575b66f0e1de1ede1ebf0",
|
|
418
|
+
conversion_type: "purchase"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
assert result[:success]
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def test_accepts_both_identifiers
|
|
425
|
+
stub_successful_conversion_response
|
|
426
|
+
|
|
427
|
+
result = Mbuzz::Client.conversion(
|
|
428
|
+
event_id: "evt_abc123",
|
|
429
|
+
visitor_id: "65dabef8...",
|
|
430
|
+
conversion_type: "purchase"
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
assert result[:success]
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Conversion type validation
|
|
437
|
+
def test_requires_conversion_type
|
|
438
|
+
result = Mbuzz::Client.conversion(
|
|
439
|
+
visitor_id: "abc123",
|
|
440
|
+
conversion_type: nil
|
|
441
|
+
)
|
|
442
|
+
assert_equal false, result
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def test_rejects_empty_conversion_type
|
|
446
|
+
result = Mbuzz::Client.conversion(
|
|
447
|
+
visitor_id: "abc123",
|
|
448
|
+
conversion_type: ""
|
|
449
|
+
)
|
|
450
|
+
assert_equal false, result
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Response handling
|
|
454
|
+
def test_returns_conversion_id_on_success
|
|
455
|
+
stub_successful_conversion_response
|
|
456
|
+
|
|
457
|
+
result = Mbuzz::Client.conversion(
|
|
458
|
+
event_id: "evt_abc123",
|
|
459
|
+
conversion_type: "purchase"
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
assert result[:conversion_id].start_with?("conv_")
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def test_returns_attribution_data
|
|
466
|
+
stub_successful_conversion_response
|
|
467
|
+
|
|
468
|
+
result = Mbuzz::Client.conversion(
|
|
469
|
+
event_id: "evt_abc123",
|
|
470
|
+
conversion_type: "purchase"
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
assert result[:attribution].is_a?(Hash)
|
|
474
|
+
assert result[:attribution][:models].key?("first_touch")
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Integration Tests (UAT)
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
# Full attribution flow test
|
|
483
|
+
visitor_id = "uat_#{SecureRandom.hex(8)}"
|
|
484
|
+
|
|
485
|
+
# Create journey with 3 sessions
|
|
486
|
+
event_ids = []
|
|
487
|
+
3.times do |i|
|
|
488
|
+
result = Mbuzz::Client.track(
|
|
489
|
+
visitor_id: visitor_id,
|
|
490
|
+
event_type: "page_view",
|
|
491
|
+
properties: {
|
|
492
|
+
session_id: "sess_#{i}_#{SecureRandom.hex(4)}",
|
|
493
|
+
utm_source: ["google", "facebook", "newsletter"][i],
|
|
494
|
+
utm_medium: ["organic", "paid", "email"][i]
|
|
495
|
+
}
|
|
496
|
+
)
|
|
497
|
+
event_ids << result[:event_id] if result[:success]
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Test event-based conversion
|
|
501
|
+
result = Mbuzz::Client.conversion(
|
|
502
|
+
event_id: event_ids.last,
|
|
503
|
+
conversion_type: "purchase",
|
|
504
|
+
revenue: 99.00
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
assert result[:success]
|
|
508
|
+
assert_equal 3, result[:attribution][:sessions_analyzed]
|
|
509
|
+
|
|
510
|
+
# Test visitor-based conversion
|
|
511
|
+
result2 = Mbuzz::Client.conversion(
|
|
512
|
+
visitor_id: visitor_id,
|
|
513
|
+
conversion_type: "upsell",
|
|
514
|
+
revenue: 49.00
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
assert result2[:success]
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Usage Examples
|
|
523
|
+
|
|
524
|
+
### Rails Controller
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
class CheckoutsController < ApplicationController
|
|
528
|
+
def create
|
|
529
|
+
@order = Order.create!(order_params)
|
|
530
|
+
|
|
531
|
+
# Option A: Event-based (recommended)
|
|
532
|
+
track_result = Mbuzz::Client.track(
|
|
533
|
+
visitor_id: mbuzz_visitor_id,
|
|
534
|
+
event_type: "checkout_completed",
|
|
535
|
+
properties: { order_id: @order.id }
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
if track_result[:success]
|
|
539
|
+
result = Mbuzz::Client.conversion(
|
|
540
|
+
event_id: track_result[:event_id],
|
|
541
|
+
conversion_type: "purchase",
|
|
542
|
+
revenue: @order.total,
|
|
543
|
+
properties: { items_count: @order.items.count }
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
@order.update!(
|
|
547
|
+
mbuzz_conversion_id: result[:conversion_id],
|
|
548
|
+
attribution_data: result[:attribution]
|
|
549
|
+
) if result[:success]
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
redirect_to order_confirmation_path(@order)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Background Job (Offline Conversion Import)
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
class ImportOfflineConversionJob < ApplicationJob
|
|
561
|
+
def perform(visitor_id, conversion_type, revenue)
|
|
562
|
+
# Visitor-based - no event tracking needed
|
|
563
|
+
result = Mbuzz::Client.conversion(
|
|
564
|
+
visitor_id: visitor_id,
|
|
565
|
+
conversion_type: conversion_type,
|
|
566
|
+
revenue: revenue,
|
|
567
|
+
properties: { source: "offline_import" }
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
Rails.logger.info "Imported conversion: #{result[:conversion_id]}" if result[:success]
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## Changelog
|
|
578
|
+
|
|
579
|
+
| Date | Change |
|
|
580
|
+
|------|--------|
|
|
581
|
+
| 2025-11-26 | Initial spec - support both `event_id` and `visitor_id` identifiers |
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
Built for mbuzz.co
|