mixpanel-ruby 2.3.0 → 3.0.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.
@@ -0,0 +1,441 @@
1
+ require 'json'
2
+ require 'mixpanel-ruby/flags/remote_flags_provider'
3
+ require 'mixpanel-ruby/flags/types'
4
+ require 'webmock/rspec'
5
+
6
+ describe Mixpanel::Flags::RemoteFlagsProvider do
7
+ let(:test_token) { 'test-token' }
8
+ let(:test_context) { { 'distinct_id' => 'user123' } }
9
+ let(:endpoint_url_regex) { %r{https://api\.mixpanel\.com/flags} }
10
+ let(:mock_tracker) { double('tracker').as_null_object }
11
+ let(:mock_error_handler) { double('error_handler', handle: nil) }
12
+ let(:config) { {} }
13
+
14
+ let(:provider) do
15
+ Mixpanel::Flags::RemoteFlagsProvider.new(
16
+ test_token,
17
+ config,
18
+ mock_tracker,
19
+ mock_error_handler
20
+ )
21
+ end
22
+
23
+ before(:each) do
24
+ WebMock.reset!
25
+ WebMock.disable_net_connect!(allow_localhost: false)
26
+ end
27
+
28
+ def create_success_response(flags_with_selected_variant)
29
+ {
30
+ code: 200,
31
+ flags: flags_with_selected_variant
32
+ }
33
+ end
34
+
35
+ def stub_flags_request(response_body)
36
+ stub_request(:get, endpoint_url_regex)
37
+ .to_return(
38
+ status: 200,
39
+ body: response_body.to_json,
40
+ headers: { 'Content-Type' => 'application/json' }
41
+ )
42
+
43
+ end
44
+
45
+ def stub_flags_request_failure(status_code)
46
+ stub_request(:get, endpoint_url_regex)
47
+ .with(basic_auth: [test_token, ''])
48
+ .to_return(status: status_code)
49
+ end
50
+
51
+ def stub_flags_request_error(error)
52
+ stub_request(:get, endpoint_url_regex)
53
+ .with(basic_auth: [test_token, ''])
54
+ .to_raise(error)
55
+ end
56
+
57
+ describe '#get_variant_value' do
58
+ it 'returns fallback value if call fails' do
59
+ stub_flags_request_error(Errno::ECONNREFUSED)
60
+
61
+ result = provider.get_variant_value('test_flag', 'control', test_context)
62
+ expect(result).to eq('control')
63
+ end
64
+
65
+ it 'returns fallback value if bad response format' do
66
+ stub_request(:get, %r{api\.mixpanel\.com/flags})
67
+ .to_return(
68
+ status: 200,
69
+ body: 'invalid json',
70
+ headers: { 'Content-Type' => 'text/plain' }
71
+ )
72
+
73
+ result = provider.get_variant_value('test_flag', 'control', test_context)
74
+ expect(result).to eq('control')
75
+ end
76
+
77
+ it 'returns fallback value if success but no flag found' do
78
+ stub_flags_request(create_success_response({}))
79
+
80
+ result = provider.get_variant_value('test_flag', 'control', test_context)
81
+ expect(result).to eq('control')
82
+ end
83
+
84
+ it 'returns expected variant from API' do
85
+ response = create_success_response({
86
+ 'test_flag' => {
87
+ 'variant_key' => 'treatment',
88
+ 'variant_value' => 'treatment'
89
+ }
90
+ })
91
+ stub_flags_request(response)
92
+
93
+ result = provider.get_variant_value('test_flag', 'control', test_context)
94
+ expect(result).to eq('treatment')
95
+ end
96
+
97
+ it 'tracks exposure event if variant selected' do
98
+ response = create_success_response({
99
+ 'test_flag' => {
100
+ 'variant_key' => 'treatment',
101
+ 'variant_value' => 'treatment'
102
+ }
103
+ })
104
+ stub_flags_request(response)
105
+
106
+ expect(mock_tracker).to receive(:call).once
107
+
108
+ provider.get_variant_value('test_flag', 'control', test_context)
109
+ end
110
+
111
+ it 'does not track exposure event if fallback' do
112
+ stub_flags_request_error(Errno::ECONNREFUSED)
113
+
114
+ expect(mock_tracker).not_to receive(:call)
115
+
116
+ provider.get_variant_value('test_flag', 'control', test_context)
117
+ end
118
+
119
+ it 'does not track exposure event when report_exposure is false' do
120
+ response = create_success_response({
121
+ 'test_flag' => {
122
+ 'variant_key' => 'treatment',
123
+ 'variant_value' => 'treatment'
124
+ }
125
+ })
126
+ stub_flags_request(response)
127
+
128
+ expect(mock_tracker).not_to receive(:call)
129
+
130
+ provider.get_variant_value('test_flag', 'control', test_context, report_exposure: false)
131
+ end
132
+
133
+ it 'handles different variant value types' do
134
+ # Test string
135
+ response = create_success_response({
136
+ 'string_flag' => {
137
+ 'variant_key' => 'treatment',
138
+ 'variant_value' => 'text-value'
139
+ }
140
+ })
141
+ stub_flags_request(response)
142
+ result = provider.get_variant_value('string_flag', 'default', test_context, report_exposure: false)
143
+ expect(result).to eq('text-value')
144
+
145
+ # Test number
146
+ WebMock.reset!
147
+ response = create_success_response({
148
+ 'number_flag' => {
149
+ 'variant_key' => 'treatment',
150
+ 'variant_value' => 42
151
+ }
152
+ })
153
+ stub_flags_request(response)
154
+ result = provider.get_variant_value('number_flag', 0, test_context, report_exposure: false)
155
+ expect(result).to eq(42)
156
+
157
+ # Test object
158
+ WebMock.reset!
159
+ response = create_success_response({
160
+ 'object_flag' => {
161
+ 'variant_key' => 'treatment',
162
+ 'variant_value' => { 'key' => 'value' }
163
+ }
164
+ })
165
+ stub_flags_request(response)
166
+ result = provider.get_variant_value('object_flag', {}, test_context, report_exposure: false)
167
+ expect(result).to eq({ 'key' => 'value' })
168
+ end
169
+ end
170
+
171
+ describe '#get_variant' do
172
+ it 'returns variant when served' do
173
+ response = create_success_response({
174
+ 'new-feature' => {
175
+ 'variant_key' => 'on',
176
+ 'variant_value' => true
177
+ }
178
+ })
179
+ stub_flags_request(response)
180
+
181
+ fallback_variant = Mixpanel::Flags::SelectedVariant.new(variant_value: false)
182
+ result = provider.get_variant('new-feature', fallback_variant, test_context, report_exposure: false)
183
+
184
+ expect(result.variant_key).to eq('on')
185
+ expect(result.variant_value).to eq(true)
186
+ end
187
+
188
+ it 'selects fallback variant when no flags are served' do
189
+ stub_flags_request(create_success_response({}))
190
+
191
+ fallback_variant = Mixpanel::Flags::SelectedVariant.new(
192
+ variant_key: 'control',
193
+ variant_value: false
194
+ )
195
+ result = provider.get_variant('any-flag', fallback_variant, test_context)
196
+
197
+ expect(result.variant_key).to eq('control')
198
+ expect(result.variant_value).to eq(false)
199
+ expect(mock_tracker).not_to have_received(:call)
200
+ end
201
+
202
+ it 'selects fallback variant if flag does not exist in served flags' do
203
+ response = create_success_response({
204
+ 'different-flag' => {
205
+ 'variant_key' => 'on',
206
+ 'variant_value' => true
207
+ }
208
+ })
209
+ stub_flags_request(response)
210
+
211
+ fallback_variant = Mixpanel::Flags::SelectedVariant.new(
212
+ variant_key: 'control',
213
+ variant_value: false
214
+ )
215
+ result = provider.get_variant('missing-flag', fallback_variant, test_context)
216
+
217
+ expect(result.variant_key).to eq('control')
218
+ expect(result.variant_value).to eq(false)
219
+ expect(mock_tracker).not_to have_received(:call)
220
+ end
221
+
222
+ it 'tracks exposure event when variant is selected' do
223
+ response = create_success_response({
224
+ 'test-flag' => {
225
+ 'variant_key' => 'treatment',
226
+ 'variant_value' => true
227
+ }
228
+ })
229
+ stub_flags_request(response)
230
+
231
+ fallback_variant = Mixpanel::Flags::SelectedVariant.new(
232
+ variant_key: 'control',
233
+ variant_value: false
234
+ )
235
+
236
+ expect(mock_tracker).to receive(:call) do |distinct_id, event_name, properties|
237
+ expect(distinct_id).to eq('user123')
238
+ expect(event_name).to eq('$experiment_started')
239
+ expect(properties['Experiment name']).to eq('test-flag')
240
+ expect(properties['Variant name']).to eq('treatment')
241
+ expect(properties['$experiment_type']).to eq('feature_flag')
242
+ expect(properties['Flag evaluation mode']).to eq('remote')
243
+ end
244
+
245
+ provider.get_variant('test-flag', fallback_variant, test_context)
246
+ end
247
+
248
+ it 'does not track exposure event when fallback variant is selected' do
249
+ stub_flags_request(create_success_response({}))
250
+
251
+ fallback_variant = Mixpanel::Flags::SelectedVariant.new(
252
+ variant_key: 'control',
253
+ variant_value: false
254
+ )
255
+
256
+ expect(mock_tracker).not_to receive(:call)
257
+
258
+ provider.get_variant('any-flag', fallback_variant, test_context)
259
+ end
260
+ end
261
+
262
+ describe '#is_enabled' do
263
+ it 'returns true when variant value is boolean true' do
264
+ response = create_success_response({
265
+ 'test_flag' => {
266
+ 'variant_key' => 'on',
267
+ 'variant_value' => true
268
+ }
269
+ })
270
+ stub_flags_request(response)
271
+
272
+ result = provider.is_enabled?('test_flag', test_context)
273
+ expect(result).to eq(true)
274
+ end
275
+
276
+ it 'returns false when variant value is boolean false' do
277
+ response = create_success_response({
278
+ 'test_flag' => {
279
+ 'variant_key' => 'off',
280
+ 'variant_value' => false
281
+ }
282
+ })
283
+ stub_flags_request(response)
284
+
285
+ result = provider.is_enabled?('test_flag', test_context)
286
+ expect(result).to eq(false)
287
+ end
288
+
289
+ it 'returns false for truthy non-boolean values' do
290
+ # Test string "true"
291
+ response = create_success_response({
292
+ 'string_flag' => {
293
+ 'variant_key' => 'treatment',
294
+ 'variant_value' => 'true'
295
+ }
296
+ })
297
+ stub_flags_request(response)
298
+
299
+ expect(mock_tracker).to receive(:call).once
300
+ result = provider.is_enabled?('string_flag', test_context)
301
+ expect(result).to eq(false)
302
+
303
+ # Test number 1
304
+ WebMock.reset!
305
+ response = create_success_response({
306
+ 'number_flag' => {
307
+ 'variant_key' => 'treatment',
308
+ 'variant_value' => 1
309
+ }
310
+ })
311
+ stub_flags_request(response)
312
+
313
+ expect(mock_tracker).to receive(:call).once
314
+ result = provider.is_enabled?('number_flag', test_context)
315
+ expect(result).to eq(false)
316
+ end
317
+
318
+ it 'returns false when flag does not exist' do
319
+ response = create_success_response({
320
+ 'different-flag' => {
321
+ 'variant_key' => 'on',
322
+ 'variant_value' => true
323
+ }
324
+ })
325
+ stub_flags_request(response)
326
+
327
+ result = provider.is_enabled?('missing-flag', test_context)
328
+ expect(result).to eq(false)
329
+ end
330
+
331
+ it 'tracks exposure event' do
332
+ response = create_success_response({
333
+ 'test_flag' => {
334
+ 'variant_key' => 'on',
335
+ 'variant_value' => true
336
+ }
337
+ })
338
+ stub_flags_request(response)
339
+
340
+ expect(mock_tracker).to receive(:call) do |distinct_id, event_name, properties|
341
+ expect(event_name).to eq('$experiment_started')
342
+ expect(properties['Experiment name']).to eq('test_flag')
343
+ expect(properties['Variant name']).to eq('on')
344
+ end
345
+
346
+ provider.is_enabled?('test_flag', test_context)
347
+ end
348
+
349
+ it 'returns false on network error' do
350
+ stub_flags_request_error(Errno::ECONNREFUSED)
351
+
352
+ result = provider.is_enabled?('test_flag', test_context)
353
+ expect(result).to eq(false)
354
+ end
355
+ end
356
+
357
+ describe '#get_all_variants' do
358
+ it 'returns all variants from API' do
359
+ response = create_success_response({
360
+ 'flag-1' => {
361
+ 'variant_key' => 'treatment',
362
+ 'variant_value' => true
363
+ },
364
+ 'flag-2' => {
365
+ 'variant_key' => 'control',
366
+ 'variant_value' => false
367
+ },
368
+ 'flag-3' => {
369
+ 'variant_key' => 'blue',
370
+ 'variant_value' => 'blue-theme'
371
+ }
372
+ })
373
+ stub_flags_request(response)
374
+
375
+ result = provider.get_all_variants(test_context)
376
+
377
+ expect(result.keys).to contain_exactly('flag-1', 'flag-2', 'flag-3')
378
+ expect(result['flag-1'].variant_key).to eq('treatment')
379
+ expect(result['flag-1'].variant_value).to eq(true)
380
+ expect(result['flag-2'].variant_key).to eq('control')
381
+ expect(result['flag-2'].variant_value).to eq(false)
382
+ expect(result['flag-3'].variant_key).to eq('blue')
383
+ expect(result['flag-3'].variant_value).to eq('blue-theme')
384
+ end
385
+
386
+ it 'returns empty hash when no flags served' do
387
+ stub_flags_request(create_success_response({}))
388
+
389
+ result = provider.get_all_variants(test_context)
390
+
391
+ expect(result).to eq({})
392
+ end
393
+
394
+ it 'does not track any exposure events' do
395
+ response = create_success_response({
396
+ 'flag-1' => {
397
+ 'variant_key' => 'treatment',
398
+ 'variant_value' => true
399
+ },
400
+ 'flag-2' => {
401
+ 'variant_key' => 'control',
402
+ 'variant_value' => false
403
+ }
404
+ })
405
+ stub_flags_request(response)
406
+
407
+ expect(mock_tracker).not_to receive(:call)
408
+
409
+ provider.get_all_variants(test_context)
410
+ end
411
+
412
+ it 'returns nil on network error' do
413
+ stub_flags_request_error(Errno::ECONNREFUSED)
414
+
415
+ result = provider.get_all_variants(test_context)
416
+
417
+ expect(result).to be_nil
418
+ end
419
+
420
+ it 'handles empty response' do
421
+ stub_flags_request(create_success_response({}))
422
+
423
+ result = provider.get_all_variants(test_context)
424
+
425
+ expect(result).to eq({})
426
+ end
427
+ end
428
+
429
+ describe '#track_exposure_event' do
430
+ it 'successfully tracks' do
431
+ variant = Mixpanel::Flags::SelectedVariant.new(
432
+ variant_key: 'treatment',
433
+ variant_value: 'treatment'
434
+ )
435
+
436
+ expect(mock_tracker).to receive(:call).once
437
+
438
+ provider.send(:track_exposure_event, 'test_flag', variant, test_context)
439
+ end
440
+ end
441
+ end
@@ -0,0 +1,110 @@
1
+ require 'rspec'
2
+ require 'mixpanel-ruby/flags/utils'
3
+
4
+ describe Mixpanel::Flags::Utils do
5
+ describe '.generate_traceparent' do
6
+ it 'should generate traceparent in W3C format' do
7
+ traceparent = Mixpanel::Flags::Utils.generate_traceparent
8
+
9
+ # W3C traceparent format: 00-{32 hex chars}-{16 hex chars}-01
10
+ # https://www.w3.org/TR/trace-context/#traceparent-header
11
+ pattern = /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/
12
+
13
+ expect(traceparent).to match(pattern)
14
+ end
15
+ end
16
+
17
+ describe '.normalized_hash' do
18
+ def expect_valid_hash(hash)
19
+ expect(hash).to be_a(Float)
20
+ expect(hash).to be >= 0.0
21
+ expect(hash).to be < 1.0
22
+ end
23
+
24
+ it 'should match known test vectors' do
25
+ hash1 = Mixpanel::Flags::Utils.normalized_hash('abc', 'variant')
26
+ expect(hash1).to eq(0.72)
27
+
28
+ hash2 = Mixpanel::Flags::Utils.normalized_hash('def', 'variant')
29
+ expect(hash2).to eq(0.21)
30
+ end
31
+
32
+ it 'should produce consistent results' do
33
+ hash1 = Mixpanel::Flags::Utils.normalized_hash('test_key', 'salt')
34
+ hash2 = Mixpanel::Flags::Utils.normalized_hash('test_key', 'salt')
35
+ hash3 = Mixpanel::Flags::Utils.normalized_hash('test_key', 'salt')
36
+
37
+ expect(hash1).to eq(hash2)
38
+ expect(hash2).to eq(hash3)
39
+ end
40
+
41
+ it 'should produce different hashes when salt is changed' do
42
+ hash1 = Mixpanel::Flags::Utils.normalized_hash('same_key', 'salt1')
43
+ hash2 = Mixpanel::Flags::Utils.normalized_hash('same_key', 'salt2')
44
+ hash3 = Mixpanel::Flags::Utils.normalized_hash('same_key', 'different_salt')
45
+
46
+ expect(hash1).not_to eq(hash2)
47
+ expect(hash1).not_to eq(hash3)
48
+ expect(hash2).not_to eq(hash3)
49
+ end
50
+
51
+ it 'should produce different hashes when order is changed' do
52
+ hash1 = Mixpanel::Flags::Utils.normalized_hash('abc', 'salt')
53
+ hash2 = Mixpanel::Flags::Utils.normalized_hash('bac', 'salt')
54
+ hash3 = Mixpanel::Flags::Utils.normalized_hash('cba', 'salt')
55
+
56
+ expect(hash1).not_to eq(hash2)
57
+ expect(hash1).not_to eq(hash3)
58
+ expect(hash2).not_to eq(hash3)
59
+ end
60
+
61
+ describe 'edge cases with empty strings' do
62
+ it 'should return valid hash for empty key' do
63
+ hash = Mixpanel::Flags::Utils.normalized_hash('', 'salt')
64
+ expect_valid_hash(hash)
65
+ end
66
+
67
+ it 'should return valid hash for empty salt' do
68
+ hash = Mixpanel::Flags::Utils.normalized_hash('key', '')
69
+ expect_valid_hash(hash)
70
+ end
71
+
72
+ it 'should return valid hash for both empty' do
73
+ hash = Mixpanel::Flags::Utils.normalized_hash('', '')
74
+ expect_valid_hash(hash)
75
+ end
76
+
77
+ it 'empty strings in different positions should produce different results' do
78
+ hash1 = Mixpanel::Flags::Utils.normalized_hash('', 'salt')
79
+ hash2 = Mixpanel::Flags::Utils.normalized_hash('key', '')
80
+ expect(hash1).not_to eq(hash2)
81
+ end
82
+ end
83
+
84
+ describe 'special characters' do
85
+ test_cases = [
86
+ { key: '🎉', description: 'emoji' },
87
+ { key: 'beyoncé', description: 'accented characters' },
88
+ { key: 'key@#$%^&*()', description: 'special symbols' },
89
+ { key: 'key with spaces', description: 'spaces' }
90
+ ]
91
+
92
+ test_cases.each do |test_case|
93
+ it "should return valid hash for #{test_case[:description]}" do
94
+ hash = Mixpanel::Flags::Utils.normalized_hash(test_case[:key], 'salt')
95
+ expect_valid_hash(hash)
96
+ end
97
+ end
98
+
99
+ it 'produces different results for different special characters' do
100
+ hashes = test_cases.map { |tc| Mixpanel::Flags::Utils.normalized_hash(tc[:key], 'salt') }
101
+
102
+ hashes.each_with_index do |hash1, i|
103
+ hashes.each_with_index do |hash2, j|
104
+ expect(hash1).not_to eq(hash2) if i != j
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -23,7 +23,7 @@ describe Mixpanel::Groups do
23
23
  '$token' => 'TEST TOKEN',
24
24
  '$group_key' => 'TEST GROUP KEY',
25
25
  '$group_id' => 'TEST GROUP ID',
26
- '$time' => @time_now.to_i * 1000,
26
+ '$time' => (@time_now.to_f * 1000).to_i,
27
27
  '$set' => {
28
28
  '$groupname' => 'Mixpanel',
29
29
  '$grouprevenue' => 200
@@ -39,7 +39,7 @@ describe Mixpanel::Groups do
39
39
  '$token' => 'TEST TOKEN',
40
40
  '$group_key' => 'TEST GROUP KEY',
41
41
  '$group_id' => 'TEST GROUP ID',
42
- '$time' => @time_now.to_i * 1000,
42
+ '$time' => (@time_now.to_f * 1000).to_i,
43
43
  '$set' => {
44
44
  'created_at' => '2013-01-02T03:04:05'
45
45
  }
@@ -54,7 +54,7 @@ describe Mixpanel::Groups do
54
54
  '$token' => 'TEST TOKEN',
55
55
  '$group_key' => 'TEST GROUP KEY',
56
56
  '$group_id' => 'TEST GROUP ID',
57
- '$time' => @time_now.to_i * 1000,
57
+ '$time' => (@time_now.to_f * 1000).to_i,
58
58
  '$set' => {
59
59
  'created_at' => '2013-01-02T02:04:05'
60
60
  }
@@ -70,7 +70,7 @@ describe Mixpanel::Groups do
70
70
  '$token' => 'TEST TOKEN',
71
71
  '$group_key' => 'TEST GROUP KEY',
72
72
  '$group_id' => 'TEST GROUP ID',
73
- '$time' => @time_now.to_i * 1000,
73
+ '$time' => (@time_now.to_f * 1000).to_i,
74
74
  '$set' => {
75
75
  'created_at' => '2013-01-02T02:04:05'
76
76
  }
@@ -86,7 +86,7 @@ describe Mixpanel::Groups do
86
86
  '$token' => 'TEST TOKEN',
87
87
  '$group_key' => 'TEST GROUP KEY',
88
88
  '$group_id' => 'TEST GROUP ID',
89
- '$time' => @time_now.to_i * 1000,
89
+ '$time' => (@time_now.to_f * 1000).to_i,
90
90
  '$set_once' => {
91
91
  '$groupname' => 'Mixpanel',
92
92
  '$grouprevenue' => 200
@@ -102,7 +102,7 @@ describe Mixpanel::Groups do
102
102
  '$token' => 'TEST TOKEN',
103
103
  '$group_key' => 'TEST GROUP KEY',
104
104
  '$group_id' => 'TEST GROUP ID',
105
- '$time' => @time_now.to_i * 1000,
105
+ '$time' => (@time_now.to_f * 1000).to_i,
106
106
  '$remove' => {
107
107
  'Albums' => 'Diamond Dogs'
108
108
  }
@@ -117,7 +117,7 @@ describe Mixpanel::Groups do
117
117
  '$token' => 'TEST TOKEN',
118
118
  '$group_key' => 'TEST GROUP KEY',
119
119
  '$group_id' => 'TEST GROUP ID',
120
- '$time' => @time_now.to_i * 1000,
120
+ '$time' => (@time_now.to_f * 1000).to_i,
121
121
  '$union' => {
122
122
  'Albums' => ['Diamond Dogs']
123
123
  }
@@ -130,7 +130,7 @@ describe Mixpanel::Groups do
130
130
  '$token' => 'TEST TOKEN',
131
131
  '$group_key' => 'TEST GROUP KEY',
132
132
  '$group_id' => 'TEST GROUP ID',
133
- '$time' => @time_now.to_i * 1000,
133
+ '$time' => (@time_now.to_f * 1000).to_i,
134
134
  '$unset' => ['Albums']
135
135
  }]])
136
136
  end
@@ -141,7 +141,7 @@ describe Mixpanel::Groups do
141
141
  '$token' => 'TEST TOKEN',
142
142
  '$group_key' => 'TEST GROUP KEY',
143
143
  '$group_id' => 'TEST GROUP ID',
144
- '$time' => @time_now.to_i * 1000,
144
+ '$time' => (@time_now.to_f * 1000).to_i,
145
145
  '$unset' => ['Albums', 'Vinyls']
146
146
  }]])
147
147
  end
@@ -152,7 +152,7 @@ describe Mixpanel::Groups do
152
152
  '$token' => 'TEST TOKEN',
153
153
  '$group_key' => 'TEST GROUP KEY',
154
154
  '$group_id' => 'TEST GROUP ID',
155
- '$time' => @time_now.to_i * 1000,
155
+ '$time' => (@time_now.to_f * 1000).to_i,
156
156
  '$delete' => ''
157
157
  }]])
158
158
  end
@@ -29,7 +29,7 @@ describe Mixpanel::Tracker do
29
29
  'mp_lib' => 'ruby',
30
30
  '$lib_version' => Mixpanel::VERSION,
31
31
  'token' => 'TEST TOKEN',
32
- 'time' => @time_now.to_i
32
+ 'time' => @time_now.to_i * 1000
33
33
  }
34
34
  expected_data = {'event' => event, 'properties' => properties.merge(default_properties)}
35
35
 
@@ -61,7 +61,7 @@ describe Mixpanel::Tracker do
61
61
  with { |req| body = req.body }
62
62
 
63
63
  message_urlencoded = body[/^data=(.*?)(?:&|$)/, 1]
64
- message_json = Base64.strict_decode64(URI.unescape(message_urlencoded))
64
+ message_json = Base64.strict_decode64(CGI.unescape(message_urlencoded))
65
65
  message = JSON.load(message_json)
66
66
  expect(message).to eq({
67
67
  'event' => 'TEST EVENT',
@@ -71,7 +71,7 @@ describe Mixpanel::Tracker do
71
71
  'mp_lib' => 'ruby',
72
72
  '$lib_version' => Mixpanel::VERSION,
73
73
  'token' => 'TEST TOKEN',
74
- 'time' => @time_now.to_i
74
+ 'time' => @time_now.to_i * 1000
75
75
  }
76
76
  })
77
77
  end
@@ -94,7 +94,7 @@ describe Mixpanel::Tracker do
94
94
  'mp_lib' => 'ruby',
95
95
  '$lib_version' => Mixpanel::VERSION,
96
96
  'token' => 'TEST TOKEN',
97
- 'time' => @time_now.to_i
97
+ 'time' => @time_now.to_i * 1000
98
98
  }
99
99
  }
100
100
  ],
@@ -106,7 +106,7 @@ describe Mixpanel::Tracker do
106
106
  'mp_lib' => 'ruby',
107
107
  '$lib_version' => Mixpanel::VERSION,
108
108
  'token' => 'TEST TOKEN',
109
- 'time' => @time_now.to_i
109
+ 'time' => @time_now.to_i * 1000
110
110
  }
111
111
  },
112
112
  'api_key' => 'API_KEY',
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,17 @@
1
+ require 'simplecov'
2
+ require 'simplecov-cobertura'
3
+
4
+ SimpleCov.start do
5
+ add_filter '/spec/'
6
+ add_filter '/demo/'
7
+
8
+ # Generate both HTML and Cobertura XML formats since CodeCov typically requires XML
9
+ formatter SimpleCov::Formatter::MultiFormatter.new([
10
+ SimpleCov::Formatter::HTMLFormatter,
11
+ SimpleCov::Formatter::CoberturaFormatter
12
+ ])
13
+ end
14
+
1
15
  require 'json'
2
16
  require 'webmock/rspec'
3
17