posthog-ruby 2.9.0 → 2.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/bin/posthog +7 -7
- data/lib/posthog/client.rb +120 -44
- data/lib/posthog/defaults.rb +4 -5
- data/lib/posthog/feature_flag.rb +27 -26
- data/lib/posthog/feature_flags.rb +247 -177
- data/lib/posthog/field_parser.rb +30 -19
- data/lib/posthog/logging.rb +1 -1
- data/lib/posthog/noop_worker.rb +2 -1
- data/lib/posthog/send_worker.rb +2 -1
- data/lib/posthog/transport.rb +8 -10
- data/lib/posthog/utils.rb +21 -28
- data/lib/posthog/version.rb +1 -1
- data/lib/posthog-ruby.rb +2 -0
- metadata +5 -105
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d987590608ff1c705f8f419d43cb6ef439233f8b3dbc5e3ac84bd64b3d552d9a
|
4
|
+
data.tar.gz: d4ae4f458a8a9faa654519b958cc20f828638ead4696dbc8a8c03ed198e44606
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9671f5f0fb37ae15a605b1118f09a526fa383aa129335a78a85f8b54c772e8c28ade9e3fd06bf36e142f7a994eca33400965add6c1e2ceebf3b8fcc32d8df23
|
7
|
+
data.tar.gz: 5fd8756e6c1f20bb0df3c40e8e72aa9c9d79dedd2bedb0c53930254d8a58417b75c38867f9e2e673bd0a53968cb7beded76cc6be18760d3fe8755d33cc75cd1b
|
data/bin/posthog
CHANGED
@@ -11,7 +11,7 @@ program :version, '1.0.0'
|
|
11
11
|
program :description, 'PostHog API'
|
12
12
|
|
13
13
|
def json_hash(str)
|
14
|
-
|
14
|
+
JSON.parse(str) if str
|
15
15
|
end
|
16
16
|
|
17
17
|
command :capture do |c|
|
@@ -27,13 +27,13 @@ command :capture do |c|
|
|
27
27
|
c.option '--event=<event>', String, 'The event name to send with the event'
|
28
28
|
c.option '--properties=<properties>', 'The properties to send (JSON-encoded)'
|
29
29
|
|
30
|
-
c.action do |
|
30
|
+
c.action do |_args, options|
|
31
31
|
posthog =
|
32
32
|
PostHog::Client.new(
|
33
33
|
{
|
34
34
|
api_key: options.api_key,
|
35
35
|
api_host: options.api_host,
|
36
|
-
on_error:
|
36
|
+
on_error: proc { |_status, msg| print msg }
|
37
37
|
}
|
38
38
|
)
|
39
39
|
|
@@ -61,13 +61,13 @@ command :identify do |c|
|
|
61
61
|
'The distinct id to send the event as'
|
62
62
|
c.option '--properties=<properties>', 'The properties to send (JSON-encoded)'
|
63
63
|
|
64
|
-
c.action do |
|
64
|
+
c.action do |_args, options|
|
65
65
|
posthog =
|
66
66
|
PostHog::Client.new(
|
67
67
|
{
|
68
68
|
api_key: options.api_key,
|
69
69
|
api_host: options.api_host,
|
70
|
-
on_error:
|
70
|
+
on_error: proc { |_status, msg| print msg }
|
71
71
|
}
|
72
72
|
)
|
73
73
|
|
@@ -92,13 +92,13 @@ command :alias do |c|
|
|
92
92
|
c.option '--distinct-id=<distinct_id>', String, 'The distinct id'
|
93
93
|
c.option '--alias=<alias>', 'The alias to give to the distinct id'
|
94
94
|
|
95
|
-
c.action do |
|
95
|
+
c.action do |_args, options|
|
96
96
|
posthog =
|
97
97
|
PostHog::Client.new(
|
98
98
|
{
|
99
99
|
api_key: options.api_key,
|
100
100
|
api_host: options.api_host,
|
101
|
-
on_error:
|
101
|
+
on_error: proc { |_status, msg| print msg }
|
102
102
|
}
|
103
103
|
)
|
104
104
|
|
data/lib/posthog/client.rb
CHANGED
@@ -22,8 +22,10 @@ class PostHog
|
|
22
22
|
# queued for testing. Defaults to +false+.
|
23
23
|
# @option opts [Proc] :on_error Handles error calls from the API.
|
24
24
|
# @option opts [String] :host Fully qualified hostname of the PostHog server. Defaults to `https://app.posthog.com`
|
25
|
-
# @option opts [Integer] :feature_flags_polling_interval How often to poll for feature flag definition changes.
|
26
|
-
#
|
25
|
+
# @option opts [Integer] :feature_flags_polling_interval How often to poll for feature flag definition changes.
|
26
|
+
# Measured in seconds, defaults to 30.
|
27
|
+
# @option opts [Integer] :feature_flag_request_timeout_seconds How long to wait for feature flag evaluation.
|
28
|
+
# Measured in seconds, defaults to 3.
|
27
29
|
def initialize(opts = {})
|
28
30
|
symbolize_keys!(opts)
|
29
31
|
|
@@ -34,10 +36,10 @@ class PostHog
|
|
34
36
|
@max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
|
35
37
|
@worker_mutex = Mutex.new
|
36
38
|
@worker = if opts[:test_mode]
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
39
|
+
NoopWorker.new(@queue)
|
40
|
+
else
|
41
|
+
SendWorker.new(@queue, @api_key, opts)
|
42
|
+
end
|
41
43
|
@worker_thread = nil
|
42
44
|
@feature_flags_poller = nil
|
43
45
|
@personal_api_key = opts[:personal_api_key]
|
@@ -53,8 +55,10 @@ class PostHog
|
|
53
55
|
opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS,
|
54
56
|
opts[:on_error]
|
55
57
|
)
|
56
|
-
|
57
|
-
@distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE)
|
58
|
+
|
59
|
+
@distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE) do |hash, key|
|
60
|
+
hash[key] = []
|
61
|
+
end
|
58
62
|
end
|
59
63
|
|
60
64
|
# Synchronously waits until the worker has cleared the queue.
|
@@ -88,6 +92,10 @@ class PostHog
|
|
88
92
|
# @option attrs [String] :event Event name
|
89
93
|
# @option attrs [Hash] :properties Event properties (optional)
|
90
94
|
# @option attrs [Bool] :send_feature_flags Whether to send feature flags with this event (optional)
|
95
|
+
# @option attrs [String] :uuid ID that uniquely identifies an event;
|
96
|
+
# events in PostHog are deduplicated by the
|
97
|
+
# combination of teamId, timestamp date,
|
98
|
+
# event name, distinct id, and UUID
|
91
99
|
# @macro common_attrs
|
92
100
|
def capture(attrs)
|
93
101
|
symbolize_keys! attrs
|
@@ -147,18 +155,34 @@ class PostHog
|
|
147
155
|
@queue.length
|
148
156
|
end
|
149
157
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
158
|
+
# TODO: In future version, rename to `feature_flag_enabled?`
|
159
|
+
def is_feature_enabled( # rubocop:disable Naming/PredicateName
|
160
|
+
flag_key,
|
161
|
+
distinct_id,
|
162
|
+
groups: {},
|
163
|
+
person_properties: {},
|
164
|
+
group_properties: {},
|
165
|
+
only_evaluate_locally: false,
|
166
|
+
send_feature_flag_events: true
|
167
|
+
)
|
168
|
+
response = get_feature_flag(
|
169
|
+
flag_key,
|
170
|
+
distinct_id,
|
171
|
+
groups: groups,
|
172
|
+
person_properties: person_properties,
|
173
|
+
group_properties: group_properties,
|
174
|
+
only_evaluate_locally: only_evaluate_locally,
|
175
|
+
send_feature_flag_events: send_feature_flag_events
|
176
|
+
)
|
177
|
+
return nil if response.nil?
|
178
|
+
|
155
179
|
!!response
|
156
180
|
end
|
157
181
|
|
158
182
|
# @param [String] flag_key The unique flag key of the feature flag
|
159
183
|
# @return [String] The decrypted value of the feature flag payload
|
160
184
|
def get_remote_config_payload(flag_key)
|
161
|
-
|
185
|
+
@feature_flags_poller.get_remote_config_payload(flag_key)
|
162
186
|
end
|
163
187
|
|
164
188
|
# Returns whether the given feature flag is enabled for the given user or not
|
@@ -168,35 +192,56 @@ class PostHog
|
|
168
192
|
# @param [Hash] groups
|
169
193
|
# @param [Hash] person_properties key-value pairs of properties to associate with the user.
|
170
194
|
# @param [Hash] group_properties
|
171
|
-
#
|
195
|
+
#
|
172
196
|
# @return [String, nil] The value of the feature flag
|
173
197
|
#
|
174
198
|
# The provided properties are used to calculate feature flags locally, if possible.
|
175
199
|
#
|
176
|
-
# `groups` are a mapping from group type to group key. So, if you have a group type of "organization"
|
200
|
+
# `groups` are a mapping from group type to group key. So, if you have a group type of "organization"
|
201
|
+
# and a group key of "5",
|
177
202
|
# you would pass groups={"organization": "5"}.
|
178
203
|
# `group_properties` take the format: { group_type_name: { group_properties } }
|
179
|
-
# So, for example, if you have the group type "organization" and the group key "5", with the properties name,
|
180
|
-
# you'll send these as:
|
204
|
+
# So, for example, if you have the group type "organization" and the group key "5", with the properties name,
|
205
|
+
# and employee count, you'll send these as:
|
181
206
|
# ```ruby
|
182
207
|
# group_properties: {"organization": {"name": "PostHog", "employees": 11}}
|
183
208
|
# ```
|
184
|
-
def get_feature_flag(
|
185
|
-
|
186
|
-
|
209
|
+
def get_feature_flag(
|
210
|
+
key,
|
211
|
+
distinct_id,
|
212
|
+
groups: {},
|
213
|
+
person_properties: {},
|
214
|
+
group_properties: {},
|
215
|
+
only_evaluate_locally: false,
|
216
|
+
send_feature_flag_events: true
|
217
|
+
)
|
218
|
+
person_properties, group_properties = add_local_person_and_group_properties(
|
219
|
+
distinct_id,
|
220
|
+
groups,
|
221
|
+
person_properties,
|
222
|
+
group_properties
|
223
|
+
)
|
224
|
+
feature_flag_response, flag_was_locally_evaluated, request_id = @feature_flags_poller.get_feature_flag(
|
225
|
+
key,
|
226
|
+
distinct_id,
|
227
|
+
groups,
|
228
|
+
person_properties,
|
229
|
+
group_properties,
|
230
|
+
only_evaluate_locally
|
231
|
+
)
|
187
232
|
|
188
233
|
feature_flag_reported_key = "#{key}_#{feature_flag_response}"
|
189
234
|
if !@distinct_id_has_sent_flag_calls[distinct_id].include?(feature_flag_reported_key) && send_feature_flag_events
|
190
235
|
capture(
|
191
236
|
{
|
192
|
-
|
193
|
-
|
194
|
-
|
237
|
+
distinct_id: distinct_id,
|
238
|
+
event: '$feature_flag_called',
|
239
|
+
properties: {
|
195
240
|
'$feature_flag' => key,
|
196
241
|
'$feature_flag_response' => feature_flag_response,
|
197
242
|
'locally_evaluated' => flag_was_locally_evaluated
|
198
|
-
}.merge(request_id ? {'$feature_flag_request_id' => request_id} : {}),
|
199
|
-
|
243
|
+
}.merge(request_id ? { '$feature_flag_request_id' => request_id } : {}),
|
244
|
+
groups: groups
|
200
245
|
}
|
201
246
|
)
|
202
247
|
@distinct_id_has_sent_flag_calls[distinct_id] << feature_flag_reported_key
|
@@ -210,11 +255,19 @@ class PostHog
|
|
210
255
|
# @param [Hash] groups
|
211
256
|
# @param [Hash] person_properties key-value pairs of properties to associate with the user.
|
212
257
|
# @param [Hash] group_properties
|
213
|
-
#
|
258
|
+
#
|
214
259
|
# @return [Hash] String (not symbol) key value pairs of flag and their values
|
215
|
-
def get_all_flags(
|
216
|
-
|
217
|
-
|
260
|
+
def get_all_flags(
|
261
|
+
distinct_id,
|
262
|
+
groups: {},
|
263
|
+
person_properties: {},
|
264
|
+
group_properties: {},
|
265
|
+
only_evaluate_locally: false
|
266
|
+
)
|
267
|
+
person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups,
|
268
|
+
person_properties, group_properties)
|
269
|
+
@feature_flags_poller.get_all_flags(distinct_id, groups, person_properties, group_properties,
|
270
|
+
only_evaluate_locally)
|
218
271
|
end
|
219
272
|
|
220
273
|
# Returns payload for a given feature flag
|
@@ -227,13 +280,23 @@ class PostHog
|
|
227
280
|
# @option [Hash] group_properties
|
228
281
|
# @option [Boolean] only_evaluate_locally
|
229
282
|
#
|
230
|
-
def get_feature_flag_payload(
|
231
|
-
|
232
|
-
|
283
|
+
def get_feature_flag_payload(
|
284
|
+
key,
|
285
|
+
distinct_id,
|
286
|
+
match_value: nil,
|
287
|
+
groups: {},
|
288
|
+
person_properties: {},
|
289
|
+
group_properties: {},
|
290
|
+
only_evaluate_locally: false
|
291
|
+
)
|
292
|
+
person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups,
|
293
|
+
person_properties, group_properties)
|
294
|
+
@feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties,
|
295
|
+
group_properties, only_evaluate_locally)
|
233
296
|
end
|
234
297
|
|
235
298
|
# Returns all flags and payloads for a given user
|
236
|
-
#
|
299
|
+
#
|
237
300
|
# @return [Hash] A hash with the following keys:
|
238
301
|
# featureFlags: A hash of feature flags
|
239
302
|
# featureFlagPayloads: A hash of feature flag payloads
|
@@ -244,9 +307,20 @@ class PostHog
|
|
244
307
|
# @option [Hash] group_properties
|
245
308
|
# @option [Boolean] only_evaluate_locally
|
246
309
|
#
|
247
|
-
def get_all_flags_and_payloads(
|
248
|
-
|
249
|
-
|
310
|
+
def get_all_flags_and_payloads(
|
311
|
+
distinct_id,
|
312
|
+
groups: {},
|
313
|
+
person_properties: {},
|
314
|
+
group_properties: {},
|
315
|
+
only_evaluate_locally: false
|
316
|
+
)
|
317
|
+
person_properties, group_properties = add_local_person_and_group_properties(
|
318
|
+
distinct_id, groups, person_properties, group_properties
|
319
|
+
)
|
320
|
+
response = @feature_flags_poller.get_all_flags_and_payloads(
|
321
|
+
distinct_id, groups, person_properties, group_properties, only_evaluate_locally
|
322
|
+
)
|
323
|
+
|
250
324
|
response.delete(:requestId) # remove internal information.
|
251
325
|
response
|
252
326
|
end
|
@@ -283,8 +357,8 @@ class PostHog
|
|
283
357
|
else
|
284
358
|
logger.warn(
|
285
359
|
'Queue is full, dropping events. The :max_queue_size ' \
|
286
|
-
|
287
|
-
|
360
|
+
'configuration parameter can be increased to prevent this from ' \
|
361
|
+
'happening.'
|
288
362
|
)
|
289
363
|
false
|
290
364
|
end
|
@@ -297,8 +371,10 @@ class PostHog
|
|
297
371
|
|
298
372
|
def ensure_worker_running
|
299
373
|
return if worker_running?
|
374
|
+
|
300
375
|
@worker_mutex.synchronize do
|
301
376
|
return if worker_running?
|
377
|
+
|
302
378
|
@worker_thread = Thread.new { @worker.run }
|
303
379
|
end
|
304
380
|
end
|
@@ -308,7 +384,6 @@ class PostHog
|
|
308
384
|
end
|
309
385
|
|
310
386
|
def add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties)
|
311
|
-
|
312
387
|
groups ||= {}
|
313
388
|
person_properties ||= {}
|
314
389
|
group_properties ||= {}
|
@@ -317,21 +392,22 @@ class PostHog
|
|
317
392
|
symbolize_keys! person_properties
|
318
393
|
symbolize_keys! group_properties
|
319
394
|
|
320
|
-
group_properties.
|
395
|
+
group_properties.each_value do |value|
|
321
396
|
symbolize_keys! value
|
322
397
|
end
|
323
398
|
|
324
|
-
all_person_properties = {
|
399
|
+
all_person_properties = { distinct_id: distinct_id }.merge(person_properties)
|
325
400
|
|
326
401
|
all_group_properties = {}
|
327
402
|
if groups
|
328
403
|
groups.each do |group_name, group_key|
|
329
404
|
all_group_properties[group_name] = {
|
330
|
-
|
405
|
+
:'$group_key' => group_key
|
406
|
+
}.merge((group_properties && group_properties[group_name]) || {})
|
331
407
|
end
|
332
408
|
end
|
333
409
|
|
334
|
-
|
410
|
+
[all_person_properties, all_group_properties]
|
335
411
|
end
|
336
412
|
end
|
337
413
|
end
|
data/lib/posthog/defaults.rb
CHANGED
@@ -1,25 +1,24 @@
|
|
1
1
|
class PostHog
|
2
2
|
module Defaults
|
3
|
-
|
4
3
|
MAX_HASH_SIZE = 50_000
|
5
4
|
|
6
5
|
module Request
|
7
|
-
HOST = 'app.posthog.com'
|
6
|
+
HOST = 'app.posthog.com'.freeze
|
8
7
|
PORT = 443
|
9
|
-
PATH = '/batch/'
|
8
|
+
PATH = '/batch/'.freeze
|
10
9
|
SSL = true
|
11
10
|
HEADERS = {
|
12
11
|
'Accept' => 'application/json',
|
13
12
|
'Content-Type' => 'application/json',
|
14
13
|
'User-Agent' => "posthog-ruby/#{PostHog::VERSION}"
|
15
|
-
}
|
14
|
+
}.freeze
|
16
15
|
RETRIES = 10
|
17
16
|
end
|
18
17
|
|
19
18
|
module FeatureFlags
|
20
19
|
FLAG_REQUEST_TIMEOUT_SECONDS = 3
|
21
20
|
end
|
22
|
-
|
21
|
+
|
23
22
|
module Queue
|
24
23
|
MAX_SIZE = 10_000
|
25
24
|
end
|
data/lib/posthog/feature_flag.rb
CHANGED
@@ -4,34 +4,35 @@ class FeatureFlag
|
|
4
4
|
|
5
5
|
def initialize(json)
|
6
6
|
json.transform_keys!(&:to_s)
|
7
|
-
@key = json[
|
8
|
-
@enabled = json[
|
9
|
-
@variant = json[
|
10
|
-
@reason = json[
|
11
|
-
@metadata = json[
|
7
|
+
@key = json['key']
|
8
|
+
@enabled = json['enabled']
|
9
|
+
@variant = json['variant']
|
10
|
+
@reason = json['reason'] ? EvaluationReason.new(json['reason']) : nil
|
11
|
+
@metadata = json['metadata'] ? FeatureFlagMetadata.new(json['metadata'].transform_keys(&:to_s)) : nil
|
12
12
|
end
|
13
13
|
|
14
|
-
|
14
|
+
# TODO: Rename to `value` in future version
|
15
|
+
def get_value # rubocop:disable Naming/AccessorMethodName
|
15
16
|
@variant || @enabled
|
16
17
|
end
|
17
18
|
|
18
19
|
def payload
|
19
|
-
@metadata
|
20
|
+
@metadata.payload if @metadata
|
20
21
|
end
|
21
22
|
|
22
23
|
def self.from_value_and_payload(key, value, payload)
|
23
24
|
new({
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
25
|
+
'key' => key,
|
26
|
+
'enabled' => value.is_a?(String) || value,
|
27
|
+
'variant' => value.is_a?(String) ? value : nil,
|
28
|
+
'reason' => nil,
|
29
|
+
'metadata' => {
|
30
|
+
'id' => nil,
|
31
|
+
'version' => nil,
|
32
|
+
'payload' => payload,
|
33
|
+
'description' => nil
|
34
|
+
}
|
35
|
+
})
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
@@ -41,9 +42,9 @@ class EvaluationReason
|
|
41
42
|
|
42
43
|
def initialize(json)
|
43
44
|
json.transform_keys!(&:to_s)
|
44
|
-
@code = json[
|
45
|
-
@description = json[
|
46
|
-
@condition_index = json[
|
45
|
+
@code = json['code']
|
46
|
+
@description = json['description']
|
47
|
+
@condition_index = json['condition_index'].to_i if json['condition_index']
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
@@ -53,9 +54,9 @@ class FeatureFlagMetadata
|
|
53
54
|
|
54
55
|
def initialize(json)
|
55
56
|
json.transform_keys!(&:to_s)
|
56
|
-
@id = json[
|
57
|
-
@version = json[
|
58
|
-
@payload = json[
|
59
|
-
@description = json[
|
57
|
+
@id = json['id']
|
58
|
+
@version = json['version']
|
59
|
+
@payload = json['payload']
|
60
|
+
@description = json['description']
|
60
61
|
end
|
61
|
-
end
|
62
|
+
end
|