posthog-ruby 3.5.4 → 3.6.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/lib/posthog/client.rb +94 -0
- data/lib/posthog/feature_flags.rb +127 -0
- data/lib/posthog/noop_worker.rb +4 -0
- data/lib/posthog/send_worker.rb +4 -0
- data/lib/posthog/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9cb5856b81b9b86c58818eb7063f4e8ca950fb1e9f9b23d998b16a43c21f9dc
|
|
4
|
+
data.tar.gz: e0d5f9a42f919ec8924b096f2c0bb3f32a5052f6b09d868ccf194dab6d5f4980
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2736eedb29171b83370b7be1eea115d015a907f2c27ed656a4ab8c708050867464a82608b1520e84d2bf045031625ae0645191395bb9992622e24ddc0dbb5e4
|
|
7
|
+
data.tar.gz: 89f33534310162d51b7edd63f99f87ef41e2b803fad8cbec9930e7c382143ae63297ac273425468b91bfbfbe863a68a06a8659364ca881ba48245a907d97bd2e
|
data/lib/posthog/client.rb
CHANGED
|
@@ -8,6 +8,8 @@ require 'posthog/logging'
|
|
|
8
8
|
require 'posthog/utils'
|
|
9
9
|
require 'posthog/send_worker'
|
|
10
10
|
require 'posthog/noop_worker'
|
|
11
|
+
require 'posthog/message_batch'
|
|
12
|
+
require 'posthog/transport'
|
|
11
13
|
require 'posthog/feature_flags'
|
|
12
14
|
require 'posthog/send_feature_flags_options'
|
|
13
15
|
require 'posthog/exception_capture'
|
|
@@ -17,6 +19,35 @@ module PostHog
|
|
|
17
19
|
include PostHog::Utils
|
|
18
20
|
include PostHog::Logging
|
|
19
21
|
|
|
22
|
+
# Thread-safe tracking of client instances per API key for singleton warnings
|
|
23
|
+
@instances_by_api_key = {}
|
|
24
|
+
@instances_mutex = Mutex.new
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Resets instance tracking. Used primarily for testing.
|
|
28
|
+
# In production, instance counts persist for the lifetime of the process.
|
|
29
|
+
def reset_instance_tracking!
|
|
30
|
+
@instances_mutex.synchronize do
|
|
31
|
+
@instances_by_api_key = {}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def _increment_instance_count(api_key)
|
|
36
|
+
@instances_mutex.synchronize do
|
|
37
|
+
count = @instances_by_api_key[api_key] || 0
|
|
38
|
+
@instances_by_api_key[api_key] = count + 1
|
|
39
|
+
count
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def _decrement_instance_count(api_key)
|
|
44
|
+
@instances_mutex.synchronize do
|
|
45
|
+
count = (@instances_by_api_key[api_key] || 1) - 1
|
|
46
|
+
@instances_by_api_key[api_key] = [count, 0].max
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
20
51
|
# @param [Hash] opts
|
|
21
52
|
# @option opts [String] :api_key Your project's api_key
|
|
22
53
|
# @option opts [String] :personal_api_key Your personal API key
|
|
@@ -24,6 +55,9 @@ module PostHog
|
|
|
24
55
|
# remain queued. Defaults to 10_000.
|
|
25
56
|
# @option opts [Bool] :test_mode +true+ if messages should remain
|
|
26
57
|
# queued for testing. Defaults to +false+.
|
|
58
|
+
# @option opts [Bool] :sync_mode +true+ to send events synchronously
|
|
59
|
+
# on the calling thread. Useful in forking environments like Sidekiq
|
|
60
|
+
# and Resque. Defaults to +false+.
|
|
27
61
|
# @option opts [Proc] :on_error Handles error calls from the API.
|
|
28
62
|
# @option opts [String] :host Fully qualified hostname of the PostHog server. Defaults to `https://app.posthog.com`
|
|
29
63
|
# @option opts [Integer] :feature_flags_polling_interval How often to poll for feature flag definition changes.
|
|
@@ -32,6 +66,8 @@ module PostHog
|
|
|
32
66
|
# Measured in seconds, defaults to 3.
|
|
33
67
|
# @option opts [Proc] :before_send A block that receives the event hash and should return either a modified hash
|
|
34
68
|
# to be sent to PostHog or nil to prevent the event from being sent. e.g. `before_send: ->(event) { event }`
|
|
69
|
+
# @option opts [Bool] :disable_singleton_warning +true+ to suppress the warning when multiple clients
|
|
70
|
+
# share the same API key. Use only when you intentionally need multiple clients. Defaults to +false+.
|
|
35
71
|
def initialize(opts = {})
|
|
36
72
|
symbolize_keys!(opts)
|
|
37
73
|
|
|
@@ -41,17 +77,42 @@ module PostHog
|
|
|
41
77
|
@api_key = opts[:api_key]
|
|
42
78
|
@max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
|
|
43
79
|
@worker_mutex = Mutex.new
|
|
80
|
+
@sync_mode = opts[:sync_mode] == true && !opts[:test_mode]
|
|
81
|
+
@on_error = opts[:on_error] || proc { |status, error| }
|
|
44
82
|
@worker = if opts[:test_mode]
|
|
45
83
|
NoopWorker.new(@queue)
|
|
84
|
+
elsif @sync_mode
|
|
85
|
+
nil
|
|
46
86
|
else
|
|
47
87
|
SendWorker.new(@queue, @api_key, opts)
|
|
48
88
|
end
|
|
89
|
+
if @sync_mode
|
|
90
|
+
@transport = Transport.new(
|
|
91
|
+
api_host: opts[:host],
|
|
92
|
+
skip_ssl_verification: opts[:skip_ssl_verification],
|
|
93
|
+
retries: 3
|
|
94
|
+
)
|
|
95
|
+
@sync_lock = Mutex.new
|
|
96
|
+
end
|
|
49
97
|
@worker_thread = nil
|
|
50
98
|
@feature_flags_poller = nil
|
|
51
99
|
@personal_api_key = opts[:personal_api_key]
|
|
52
100
|
|
|
53
101
|
check_api_key!
|
|
54
102
|
|
|
103
|
+
# Warn when multiple clients are created with the same API key (can cause dropped events)
|
|
104
|
+
unless opts[:test_mode] || opts[:disable_singleton_warning]
|
|
105
|
+
previous_count = self.class._increment_instance_count(@api_key)
|
|
106
|
+
if previous_count >= 1
|
|
107
|
+
logger.warn(
|
|
108
|
+
'Multiple PostHog client instances detected for the same API key. ' \
|
|
109
|
+
'This can cause dropped events and inconsistent behavior. ' \
|
|
110
|
+
'Use a singleton pattern: instantiate once and reuse the client. ' \
|
|
111
|
+
'See https://posthog.com/docs/libraries/ruby'
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
55
116
|
@feature_flags_poller =
|
|
56
117
|
FeatureFlagsPoller.new(
|
|
57
118
|
opts[:feature_flags_polling_interval],
|
|
@@ -74,6 +135,12 @@ module PostHog
|
|
|
74
135
|
# Use only for scripts which are not long-running, and will specifically
|
|
75
136
|
# exit
|
|
76
137
|
def flush
|
|
138
|
+
if @sync_mode
|
|
139
|
+
# Wait for any in-flight sync send to complete
|
|
140
|
+
@sync_lock.synchronize {} # rubocop:disable Lint/EmptyBlock
|
|
141
|
+
return
|
|
142
|
+
end
|
|
143
|
+
|
|
77
144
|
while !@queue.empty? || @worker.is_requesting?
|
|
78
145
|
ensure_worker_running
|
|
79
146
|
sleep(0.1)
|
|
@@ -444,8 +511,14 @@ module PostHog
|
|
|
444
511
|
end
|
|
445
512
|
|
|
446
513
|
def shutdown
|
|
514
|
+
self.class._decrement_instance_count(@api_key) if @api_key
|
|
447
515
|
@feature_flags_poller.shutdown_poller
|
|
448
516
|
flush
|
|
517
|
+
if @sync_mode
|
|
518
|
+
@sync_lock.synchronize { @transport&.shutdown }
|
|
519
|
+
else
|
|
520
|
+
@worker&.shutdown
|
|
521
|
+
end
|
|
449
522
|
end
|
|
450
523
|
|
|
451
524
|
private
|
|
@@ -483,6 +556,11 @@ module PostHog
|
|
|
483
556
|
# add our request id for tracing purposes
|
|
484
557
|
action[:messageId] ||= uid
|
|
485
558
|
|
|
559
|
+
if @sync_mode
|
|
560
|
+
send_sync(action)
|
|
561
|
+
return true
|
|
562
|
+
end
|
|
563
|
+
|
|
486
564
|
if @queue.length < @max_queue_size
|
|
487
565
|
@queue << action
|
|
488
566
|
ensure_worker_running
|
|
@@ -513,6 +591,22 @@ module PostHog
|
|
|
513
591
|
end
|
|
514
592
|
end
|
|
515
593
|
|
|
594
|
+
def send_sync(action)
|
|
595
|
+
batch = MessageBatch.new(1)
|
|
596
|
+
begin
|
|
597
|
+
batch << action
|
|
598
|
+
rescue MessageBatch::JSONGenerationError => e
|
|
599
|
+
@on_error.call(-1, e.to_s)
|
|
600
|
+
return
|
|
601
|
+
end
|
|
602
|
+
return if batch.empty?
|
|
603
|
+
|
|
604
|
+
@sync_lock.synchronize do
|
|
605
|
+
res = @transport.send(@api_key, batch)
|
|
606
|
+
@on_error.call(res.status, res.error) unless res.status == 200
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
516
610
|
def worker_running?
|
|
517
611
|
@worker_thread&.alive?
|
|
518
612
|
end
|
|
@@ -422,6 +422,103 @@ module PostHog
|
|
|
422
422
|
parsed_dt
|
|
423
423
|
end
|
|
424
424
|
|
|
425
|
+
# Parse a semver string into a comparable [major, minor, patch] integer array.
|
|
426
|
+
# Handles v-prefix, whitespace, pre-release suffixes. Defaults missing components to 0.
|
|
427
|
+
def self.parse_semver(value)
|
|
428
|
+
raise InconclusiveMatchError, 'Invalid semver format' if value.nil?
|
|
429
|
+
|
|
430
|
+
text = value.to_s.strip.sub(/^[vV]/, '')
|
|
431
|
+
|
|
432
|
+
raise InconclusiveMatchError, 'Invalid semver format' if text.empty?
|
|
433
|
+
|
|
434
|
+
# Strip pre-release and build metadata suffixes
|
|
435
|
+
text = text.split('-')[0].split('+')[0]
|
|
436
|
+
parts = text.split('.')
|
|
437
|
+
|
|
438
|
+
raise InconclusiveMatchError, 'Invalid semver format' if parts.empty? || parts[0].to_s.empty?
|
|
439
|
+
|
|
440
|
+
# Check for leading dot or non-numeric parts
|
|
441
|
+
parts.each do |part|
|
|
442
|
+
raise InconclusiveMatchError, 'Invalid semver format' if part.empty? || part !~ /^\d+$/
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
major = parts[0].to_i
|
|
446
|
+
minor = parts.length > 1 ? parts[1].to_i : 0
|
|
447
|
+
patch = parts.length > 2 ? parts[2].to_i : 0
|
|
448
|
+
|
|
449
|
+
[major, minor, patch]
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Returns bounds for tilde (~) range:
|
|
453
|
+
# ~X → >=X.0.0 <(X+1).0.0
|
|
454
|
+
# ~X.Y → >=X.Y.0 <X.(Y+1).0
|
|
455
|
+
# ~X.Y.Z → >=X.Y.Z <X.(Y+1).0
|
|
456
|
+
def self.semver_tilde_bounds(value)
|
|
457
|
+
major, minor, patch = parse_semver(value)
|
|
458
|
+
lower = [major, minor, patch]
|
|
459
|
+
|
|
460
|
+
# Determine how many components were provided
|
|
461
|
+
text = value.to_s.strip.sub(/^[vV]/, '')
|
|
462
|
+
text = text.split('-')[0].split('+')[0]
|
|
463
|
+
component_count = text.split('.').length
|
|
464
|
+
|
|
465
|
+
upper = if component_count == 1
|
|
466
|
+
# Major-only: bump major
|
|
467
|
+
[major + 1, 0, 0]
|
|
468
|
+
else
|
|
469
|
+
# Major.minor or major.minor.patch: bump minor
|
|
470
|
+
[major, minor + 1, 0]
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
[lower, upper]
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Returns bounds for caret (^) range per semver spec:
|
|
477
|
+
# ^X.Y.Z where X > 0 → >=X.Y.Z <(X+1).0.0
|
|
478
|
+
# ^0.Y.Z where Y > 0 → >=0.Y.Z <0.(Y+1).0
|
|
479
|
+
# ^0.0.Z → >=0.0.Z <0.0.(Z+1)
|
|
480
|
+
def self.semver_caret_bounds(value)
|
|
481
|
+
major, minor, patch = parse_semver(value)
|
|
482
|
+
lower = [major, minor, patch]
|
|
483
|
+
|
|
484
|
+
upper = if major.positive?
|
|
485
|
+
[major + 1, 0, 0]
|
|
486
|
+
elsif minor.positive?
|
|
487
|
+
[0, minor + 1, 0]
|
|
488
|
+
else
|
|
489
|
+
[0, 0, patch + 1]
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
[lower, upper]
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Returns bounds for wildcard (*) range:
|
|
496
|
+
# X.* or X → >=X.0.0 <(X+1).0.0
|
|
497
|
+
# X.Y.* → >=X.Y.0 <X.(Y+1).0
|
|
498
|
+
def self.semver_wildcard_bounds(value)
|
|
499
|
+
cleaned = value.to_s.strip.sub(/^[vV]/, '').gsub('*', '').chomp('.')
|
|
500
|
+
parts = cleaned.split('.').reject(&:empty?)
|
|
501
|
+
|
|
502
|
+
raise InconclusiveMatchError, 'Invalid semver wildcard format' if parts.empty?
|
|
503
|
+
|
|
504
|
+
parts.each do |part|
|
|
505
|
+
raise InconclusiveMatchError, 'Invalid semver wildcard format' if part !~ /^\d+$/
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
major = parts[0].to_i
|
|
509
|
+
case parts.length
|
|
510
|
+
when 1
|
|
511
|
+
[[major, 0, 0], [major + 1, 0, 0]]
|
|
512
|
+
when 2
|
|
513
|
+
minor = parts[1].to_i
|
|
514
|
+
[[major, minor, 0], [major, minor + 1, 0]]
|
|
515
|
+
else
|
|
516
|
+
minor = parts[1].to_i
|
|
517
|
+
patch = parts[2].to_i
|
|
518
|
+
[[major, minor, patch], [major, minor, patch + 1]]
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
425
522
|
def self.match_property(property, property_values, cohort_properties = {})
|
|
426
523
|
# only looks for matches where key exists in property_values
|
|
427
524
|
# doesn't support operator is_not_set
|
|
@@ -496,6 +593,36 @@ module PostHog
|
|
|
496
593
|
elsif operator == 'is_date_after'
|
|
497
594
|
override_date > parsed_date
|
|
498
595
|
end
|
|
596
|
+
when 'semver_eq', 'semver_neq', 'semver_gt', 'semver_gte', 'semver_lt', 'semver_lte'
|
|
597
|
+
override_parsed = parse_semver(override_value)
|
|
598
|
+
flag_parsed = parse_semver(value)
|
|
599
|
+
|
|
600
|
+
case operator
|
|
601
|
+
when 'semver_eq'
|
|
602
|
+
override_parsed == flag_parsed
|
|
603
|
+
when 'semver_neq'
|
|
604
|
+
override_parsed != flag_parsed
|
|
605
|
+
when 'semver_gt'
|
|
606
|
+
(override_parsed <=> flag_parsed) == 1
|
|
607
|
+
when 'semver_gte'
|
|
608
|
+
(override_parsed <=> flag_parsed) >= 0
|
|
609
|
+
when 'semver_lt'
|
|
610
|
+
(override_parsed <=> flag_parsed) == -1
|
|
611
|
+
when 'semver_lte'
|
|
612
|
+
(override_parsed <=> flag_parsed) <= 0
|
|
613
|
+
end
|
|
614
|
+
when 'semver_tilde'
|
|
615
|
+
override_parsed = parse_semver(override_value)
|
|
616
|
+
lower, upper = semver_tilde_bounds(value)
|
|
617
|
+
(override_parsed <=> lower) >= 0 && (override_parsed <=> upper) == -1
|
|
618
|
+
when 'semver_caret'
|
|
619
|
+
override_parsed = parse_semver(override_value)
|
|
620
|
+
lower, upper = semver_caret_bounds(value)
|
|
621
|
+
(override_parsed <=> lower) >= 0 && (override_parsed <=> upper) == -1
|
|
622
|
+
when 'semver_wildcard'
|
|
623
|
+
override_parsed = parse_semver(override_value)
|
|
624
|
+
lower, upper = semver_wildcard_bounds(value)
|
|
625
|
+
(override_parsed <=> lower) >= 0 && (override_parsed <=> upper) == -1
|
|
499
626
|
else
|
|
500
627
|
raise InconclusiveMatchError, "Unknown operator: #{operator}"
|
|
501
628
|
end
|
data/lib/posthog/noop_worker.rb
CHANGED
data/lib/posthog/send_worker.rb
CHANGED
data/lib/posthog/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: posthog-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
@@ -72,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
72
72
|
- !ruby/object:Gem::Version
|
|
73
73
|
version: '0'
|
|
74
74
|
requirements: []
|
|
75
|
-
rubygems_version: 4.0.
|
|
75
|
+
rubygems_version: 4.0.6
|
|
76
76
|
specification_version: 4
|
|
77
77
|
summary: PostHog library
|
|
78
78
|
test_files: []
|