mbuzz 0.8.2 → 0.10.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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +43 -0
- data/lib/mbuzz/client/conversion_request.rb +6 -1
- data/lib/mbuzz/client/track_request.rb +8 -1
- data/lib/mbuzz/dropped_call.rb +41 -0
- data/lib/mbuzz/middleware/session_endpoint.rb +135 -0
- data/lib/mbuzz/middleware/tracking.rb +12 -35
- data/lib/mbuzz/railtie.rb +3 -0
- data/lib/mbuzz/version.rb +1 -1
- data/lib/mbuzz.rb +19 -4
- metadata +3 -11
- data/.DS_Store +0 -0
- data/CHECK_BUG.md +0 -168
- data/lib/.DS_Store +0 -0
- data/lib/specs/old/SPECIFICATION.md +0 -695
- data/lib/specs/old/conversions.md +0 -585
- data/lib/specs/old/event_ids_response.md +0 -346
- data/lib/specs/old/v0.2.0_breaking_changes.md +0 -519
- data/lib/specs/old/v2.0.0_sessions_upgrade.md +0 -265
- data/lib/specs/v0.5.0_four_call_model.md +0 -505
- data/lib/specs/v0.7.0_deterministic_sessions.md +0 -647
data/CHECK_BUG.md
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# Thread-Safety Bug Fix Verification
|
|
2
|
-
|
|
3
|
-
## Bug Summary
|
|
4
|
-
|
|
5
|
-
**Issue**: Middleware used instance variables (`@session_id`, `@visitor_id`, `@request`) shared across concurrent requests in multi-threaded servers (Puma). This caused race conditions where session/visitor IDs leaked between requests.
|
|
6
|
-
|
|
7
|
-
**Impact**: Pet Resorts Australia had **178,428 sessions** for only **172,000 visitors**. Some visitors had 1,500+ sessions because cookies were set with wrong session_ids under concurrent load.
|
|
8
|
-
|
|
9
|
-
**Root Cause**: Rack middleware is instantiated once and shared across all requests. Instance variables are not thread-safe.
|
|
10
|
-
|
|
11
|
-
## Fix Details
|
|
12
|
-
|
|
13
|
-
| Field | Value |
|
|
14
|
-
|-------|-------|
|
|
15
|
-
| **Fixed in version** | 0.6.3 |
|
|
16
|
-
| **Commit** | `bdf4c64` |
|
|
17
|
-
| **Fix deployed** | 2025-12-22 ~21:30 UTC (2025-12-23 ~08:30 AEDT) |
|
|
18
|
-
| **Gem published** | 2025-12-23 |
|
|
19
|
-
|
|
20
|
-
## Verification Checklist
|
|
21
|
-
|
|
22
|
-
### After 24-48 hours (by 2025-12-25):
|
|
23
|
-
|
|
24
|
-
Run these queries in production Rails console:
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
# 1. Check session creation rate AFTER fix
|
|
28
|
-
# Should see dramatically fewer sessions per hour
|
|
29
|
-
cutoff = Time.parse("2025-12-23 08:30:00 UTC") # Adjust to actual deploy time
|
|
30
|
-
|
|
31
|
-
puts "Sessions BEFORE fix (last 24h before deploy):"
|
|
32
|
-
before_sessions = Session.where(created_at: (cutoff - 24.hours)..cutoff).count
|
|
33
|
-
puts " Count: #{before_sessions}"
|
|
34
|
-
|
|
35
|
-
puts "\nSessions AFTER fix (24h after deploy):"
|
|
36
|
-
after_sessions = Session.where(created_at: cutoff..(cutoff + 24.hours)).count
|
|
37
|
-
puts " Count: #{after_sessions}"
|
|
38
|
-
|
|
39
|
-
puts "\nReduction: #{((before_sessions - after_sessions).to_f / before_sessions * 100).round(1)}%"
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
```ruby
|
|
43
|
-
# 2. Check sessions per visitor ratio
|
|
44
|
-
# Should be close to 1.0-1.5 for new visitors (was 1.05 overall but outliers had 1500+)
|
|
45
|
-
cutoff = Time.parse("2025-12-23 08:30:00 UTC")
|
|
46
|
-
|
|
47
|
-
new_visitors = Visitor.where("created_at > ?", cutoff)
|
|
48
|
-
new_visitor_ids = new_visitors.pluck(:id)
|
|
49
|
-
|
|
50
|
-
sessions_for_new = Session.where(visitor_id: new_visitor_ids).count
|
|
51
|
-
puts "New visitors since fix: #{new_visitors.count}"
|
|
52
|
-
puts "Sessions for new visitors: #{sessions_for_new}"
|
|
53
|
-
puts "Ratio: #{(sessions_for_new.to_f / new_visitors.count).round(2)}"
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
```ruby
|
|
57
|
-
# 3. Check for any new outliers (visitors with 10+ sessions in 24h)
|
|
58
|
-
cutoff = Time.parse("2025-12-23 08:30:00 UTC")
|
|
59
|
-
|
|
60
|
-
outliers = Session.where("created_at > ?", cutoff)
|
|
61
|
-
.group(:visitor_id)
|
|
62
|
-
.having("count(*) > 10")
|
|
63
|
-
.count
|
|
64
|
-
|
|
65
|
-
puts "Visitors with 10+ sessions since fix: #{outliers.count}"
|
|
66
|
-
outliers.sort_by { |_, v| -v }.first(5).each do |vid, count|
|
|
67
|
-
puts " Visitor #{vid}: #{count} sessions"
|
|
68
|
-
end
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Expected Results After Fix:
|
|
72
|
-
|
|
73
|
-
- [ ] Session creation rate drops by 90%+
|
|
74
|
-
- [ ] Sessions per new visitor ratio < 2.0
|
|
75
|
-
- [ ] No new outliers with 100+ sessions
|
|
76
|
-
- [ ] Cookie session_id matches env session_id (verified by tests)
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## Other SDKs to Review
|
|
81
|
-
|
|
82
|
-
**CRITICAL**: Check all other SDKs for the same thread-safety bug!
|
|
83
|
-
|
|
84
|
-
### SDK Review Checklist:
|
|
85
|
-
|
|
86
|
-
| SDK | Location | Status | Reviewed By | Date |
|
|
87
|
-
|-----|----------|--------|-------------|------|
|
|
88
|
-
| mbuzz-ruby | `/Users/vlad/code/m/mbuzz-ruby` | FIXED | Claude | 2025-12-22 |
|
|
89
|
-
| mbuzz-python | `/Users/vlad/code/m/mbuzz-python` | SAFE | Claude | 2025-12-22 |
|
|
90
|
-
| mbuzz-php | `/Users/vlad/code/m/mbuzz-php` | SAFE | Claude | 2025-12-22 |
|
|
91
|
-
| mbuzz-node | `/Users/vlad/code/m/mbuzz-node` | SAFE | Claude | 2025-12-22 |
|
|
92
|
-
|
|
93
|
-
### Review Results:
|
|
94
|
-
|
|
95
|
-
**mbuzz-python**: SAFE
|
|
96
|
-
- Uses `contextvars.ContextVar` for thread-safe context storage
|
|
97
|
-
- Uses Flask's `g` object for request-scoped storage
|
|
98
|
-
- Local variables used throughout middleware
|
|
99
|
-
- Async session creation captures values in local variables before spawning thread
|
|
100
|
-
|
|
101
|
-
**mbuzz-php**: SAFE
|
|
102
|
-
- PHP is single-process per request by default
|
|
103
|
-
- No shared state between requests
|
|
104
|
-
- Each request gets fresh instance of everything
|
|
105
|
-
|
|
106
|
-
**mbuzz-node**: SAFE
|
|
107
|
-
- Uses `AsyncLocalStorage` from `node:async_hooks` for async request isolation
|
|
108
|
-
- Express middleware uses local variables (`visitor`, `session`, `secure`)
|
|
109
|
-
- Attaches data to request-scoped `req.mbuzz` object
|
|
110
|
-
- `createSessionAsync` captures values as function parameters before `setImmediate`
|
|
111
|
-
- Node.js is single-threaded, so race conditions are inherently less likely
|
|
112
|
-
|
|
113
|
-
### What to Look For:
|
|
114
|
-
|
|
115
|
-
1. **Middleware/Handler using instance variables or class variables for request-specific data**
|
|
116
|
-
- BAD: `self.session_id = ...` or `@session_id = ...`
|
|
117
|
-
- GOOD: Local variables passed through function calls
|
|
118
|
-
|
|
119
|
-
2. **Mutable shared state**
|
|
120
|
-
- BAD: Global or class-level dicts/hashes storing request data
|
|
121
|
-
- GOOD: Request-scoped context objects or local variables
|
|
122
|
-
|
|
123
|
-
3. **Thread-local storage without proper cleanup**
|
|
124
|
-
- Check that thread-local data is cleared after each request
|
|
125
|
-
|
|
126
|
-
### Python-specific concerns:
|
|
127
|
-
- Check for module-level variables
|
|
128
|
-
- Check Flask/Django middleware for shared state
|
|
129
|
-
- WSGI apps can have similar issues with global state
|
|
130
|
-
|
|
131
|
-
### PHP-specific concerns:
|
|
132
|
-
- PHP is typically single-threaded per request, so likely SAFE
|
|
133
|
-
- But check for any persistent worker modes (Swoole, RoadRunner, FrankenPHP)
|
|
134
|
-
|
|
135
|
-
### Node.js-specific concerns:
|
|
136
|
-
- Node is single-threaded, so likely SAFE
|
|
137
|
-
- But check for any shared state in closures or module scope
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
## Data Cleanup (Optional)
|
|
142
|
-
|
|
143
|
-
After verifying the fix works, consider cleaning up the bad data:
|
|
144
|
-
|
|
145
|
-
```ruby
|
|
146
|
-
# Find sessions with no events (likely created by the bug)
|
|
147
|
-
# BE CAREFUL - only run after thorough analysis
|
|
148
|
-
|
|
149
|
-
# Count empty sessions by account
|
|
150
|
-
Account.find_each do |account|
|
|
151
|
-
session_ids_with_events = account.events.distinct.pluck(:session_id)
|
|
152
|
-
empty_sessions = account.sessions.where.not(session_id: session_ids_with_events).count
|
|
153
|
-
total_sessions = account.sessions.count
|
|
154
|
-
|
|
155
|
-
next if empty_sessions == 0
|
|
156
|
-
|
|
157
|
-
puts "#{account.name}: #{empty_sessions}/#{total_sessions} empty sessions (#{(empty_sessions.to_f/total_sessions*100).round(1)}%)"
|
|
158
|
-
end
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
## Notes
|
|
164
|
-
|
|
165
|
-
- Bug was discovered via dashboard metrics investigation (avg visits showing 28.5 with 0.6 avg days)
|
|
166
|
-
- Traced to Pet Resorts Australia account (PetPro360)
|
|
167
|
-
- Logs showed session creation every few seconds with different session_ids
|
|
168
|
-
- Test added: `test_race_condition_with_slow_app` - 49/50 failures before fix, 0 after
|
data/lib/.DS_Store
DELETED
|
Binary file
|