Skip to content

Commit fb5495c

Browse files
committed
feat: implement weighted random selection based on market share
1 parent bb10d8c commit fb5495c

2 files changed

Lines changed: 105 additions & 27 deletions

File tree

useragent.go

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -179,38 +179,20 @@ func (m *Manager) GetAllMobile() []UserAgent {
179179
return agents
180180
}
181181

182-
// GetRandomDesktop returns a random desktop UserAgent using crypto/rand
182+
// GetRandomDesktop returns a random desktop UserAgent using weighted random selection
183183
func (m *Manager) GetRandomDesktop() (UserAgent, error) {
184184
m.mu.RLock()
185185
defer m.mu.RUnlock()
186186

187-
if len(m.desktopAgents) == 0 {
188-
return UserAgent{}, ErrEmptyAgentList
189-
}
190-
191-
idx, err := secureRandomInt(len(m.desktopAgents))
192-
if err != nil {
193-
return UserAgent{}, fmt.Errorf("failed to generate random index: %w", err)
194-
}
195-
196-
return m.desktopAgents[idx], nil
187+
return m.getWeightedRandomAgent(m.desktopAgents)
197188
}
198189

199-
// GetRandomMobile returns a random mobile UserAgent using crypto/rand
190+
// GetRandomMobile returns a random mobile UserAgent using weighted random selection
200191
func (m *Manager) GetRandomMobile() (UserAgent, error) {
201192
m.mu.RLock()
202193
defer m.mu.RUnlock()
203194

204-
if len(m.mobileAgents) == 0 {
205-
return UserAgent{}, ErrEmptyAgentList
206-
}
207-
208-
idx, err := secureRandomInt(len(m.mobileAgents))
209-
if err != nil {
210-
return UserAgent{}, fmt.Errorf("failed to generate random index: %w", err)
211-
}
212-
213-
return m.mobileAgents[idx], nil
195+
return m.getWeightedRandomAgent(m.mobileAgents)
214196
}
215197

216198
// GetRandomDesktopUA returns just the UA string of a random desktop user agent
@@ -236,20 +218,62 @@ func (m *Manager) GetRandomUA() (string, error) {
236218
m.mu.RLock()
237219
defer m.mu.RUnlock()
238220

221+
// Combine agents for weighted selection across all
222+
// Note: This assumes percentages in both files are relative to their own category (sum to ~100)
223+
// If we want to mix them, we might need to normalize or just treat them as one pool.
224+
// For simplicity and robustness, let's treat them as one big pool where weights are relative.
225+
239226
allAgents := make([]UserAgent, 0, len(m.desktopAgents)+len(m.mobileAgents))
240227
allAgents = append(allAgents, m.desktopAgents...)
241228
allAgents = append(allAgents, m.mobileAgents...)
242229

243-
if len(allAgents) == 0 {
244-
return "", ErrEmptyAgentList
230+
ua, err := m.getWeightedRandomAgent(allAgents)
231+
if err != nil {
232+
return "", err
233+
}
234+
return ua.UA, nil
235+
}
236+
237+
// getWeightedRandomAgent selects an agent based on its Pct value
238+
func (m *Manager) getWeightedRandomAgent(agents []UserAgent) (UserAgent, error) {
239+
if len(agents) == 0 {
240+
return UserAgent{}, ErrEmptyAgentList
241+
}
242+
243+
var totalWeight float64
244+
for _, ua := range agents {
245+
totalWeight += ua.Pct
245246
}
246247

247-
idx, err := secureRandomInt(len(allAgents))
248+
if totalWeight <= 0 {
249+
// Fallback to uniform selection if weights are invalid
250+
idx, err := secureRandomInt(len(agents))
251+
if err != nil {
252+
return UserAgent{}, err
253+
}
254+
return agents[idx], nil
255+
}
256+
257+
// Generate a random value in [0, totalWeight)
258+
// We use a large integer range for precision
259+
const precision = 1_000_000
260+
randInt, err := secureRandomInt(precision)
248261
if err != nil {
249-
return "", fmt.Errorf("failed to generate random index: %w", err)
262+
return UserAgent{}, fmt.Errorf("failed to generate random value: %w", err)
263+
}
264+
265+
randomWeight := float64(randInt) / float64(precision) * totalWeight
266+
267+
currentWeight := 0.0
268+
for _, ua := range agents {
269+
currentWeight += ua.Pct
270+
if randomWeight < currentWeight {
271+
return ua, nil
272+
}
250273
}
251274

252-
return allAgents[idx].UA, nil
275+
// Should not happen if logic is correct, but return last one as fallback
276+
return agents[len(agents)-1], nil
253277
}
254278

255279
// secureRandomInt generates a cryptographically secure random integer in [0, max)

weighted_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package commonuseragent
2+
3+
import (
4+
"math"
5+
"testing"
6+
)
7+
8+
func TestWeightedRandomSelection(t *testing.T) {
9+
// Use a custom manager with known weights for testing
10+
cfg := Config{
11+
DesktopFile: "desktop_useragents.json", // We'll override the data anyway
12+
MobileFile: "mobile_useragents.json",
13+
}
14+
mgr, err := NewManager(cfg)
15+
if err != nil {
16+
t.Fatalf("Failed to create manager: %v", err)
17+
}
18+
19+
// Override agents with known weights
20+
mgr.desktopAgents = []UserAgent{
21+
{UA: "A", Pct: 10.0},
22+
{UA: "B", Pct: 30.0},
23+
{UA: "C", Pct: 60.0},
24+
}
25+
26+
iterations := 10000
27+
counts := make(map[string]int)
28+
29+
for i := 0; i < iterations; i++ {
30+
ua, err := mgr.GetRandomDesktop()
31+
if err != nil {
32+
t.Fatalf("GetRandomDesktop failed: %v", err)
33+
}
34+
counts[ua.UA]++
35+
}
36+
37+
// Check if distribution is roughly correct (within 5% margin of error)
38+
expected := map[string]float64{
39+
"A": 0.10,
40+
"B": 0.30,
41+
"C": 0.60,
42+
}
43+
44+
for ua, count := range counts {
45+
observedPct := float64(count) / float64(iterations)
46+
expectedPct := expected[ua]
47+
diff := math.Abs(observedPct - expectedPct)
48+
49+
// Allow 2% deviation
50+
if diff > 0.02 {
51+
t.Errorf("Distribution for %s incorrect. Expected %.2f, got %.2f", ua, expectedPct, observedPct)
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)