posthog-ruby 2.9.0 → 2.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9be467263d4138e0b223e18281a9b0af56351fbf9d807b01bf4ff8f87d004a7
4
- data.tar.gz: 1c4a9990d2958023b7db53511e64aa90ade5364d06e42e23b2b981d6acb59ba2
3
+ metadata.gz: 952a3d08e42e35c4006614c4ea33cb0fb7216c250f2eade20bb6c37d12fb5b13
4
+ data.tar.gz: 9f4773ec646c3e7e4f5183d818245d0dab1f275ec5f01de1ddcc073483499fbb
5
5
  SHA512:
6
- metadata.gz: abde921140981a63fb82677e5024c4fcce79f9e560a870b0fd91114eaff0df1af0a5908742fbab3dee612d1d1a866998e9f1d10a587484e4baaf06b2f7cafd45
7
- data.tar.gz: 7d88779cd59527ba500d8a2dcee0692a7135da88910d8045b276f32b635caee887b3fe2382a85fd6408b0e8cd57c58e0ca84495c99f912763a13f2b62e01dc78
6
+ metadata.gz: f3b66458d3987de0296702f9ef32a3ec90485fb263a986ff7e703ee6ca5113be320acb28fa07b48231401898e630c24f2c11067e6a01aa5bc7860ce494b15a40
7
+ data.tar.gz: 92d1b482dbe35e57c30cbc0160a6be1f1589cb63a8871eaaefdde8c64aa49d8d1d280a05d6ecd4f55fb1a850e62f73c756878081d53b6bede4b21c9859cfd006
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
- return JSON.parse(str) if str
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 |args, options|
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: Proc.new { |status, msg| print msg }
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 |args, options|
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: Proc.new { |status, msg| print msg }
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 |args, options|
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: Proc.new { |status, msg| print msg }
101
+ on_error: proc { |_status, msg| print msg }
102
102
  }
103
103
  )
104
104
 
@@ -22,8 +22,12 @@ 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. Measured in seconds, defaults to 30.
26
- # @option opts [Integer] :feature_flag_request_timeout_seconds How long to wait for feature flag evaluation. Measured in seconds, defaults to 3.
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.
29
+ # @option opts [Proc] :before_send A block that receives the event hash and should return either a modified hash
30
+ # to be sent to PostHog or nil to prevent the event from being sent. e.g. `before_send: ->(event) { event }`
27
31
  def initialize(opts = {})
28
32
  symbolize_keys!(opts)
29
33
 
@@ -34,10 +38,10 @@ class PostHog
34
38
  @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
35
39
  @worker_mutex = Mutex.new
36
40
  @worker = if opts[:test_mode]
37
- NoopWorker.new(@queue)
38
- else
39
- SendWorker.new(@queue, @api_key, opts)
40
- end
41
+ NoopWorker.new(@queue)
42
+ else
43
+ SendWorker.new(@queue, @api_key, opts)
44
+ end
41
45
  @worker_thread = nil
42
46
  @feature_flags_poller = nil
43
47
  @personal_api_key = opts[:personal_api_key]
@@ -53,8 +57,12 @@ class PostHog
53
57
  opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS,
54
58
  opts[:on_error]
55
59
  )
56
-
57
- @distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE) { |hash, key| hash[key] = Array.new }
60
+
61
+ @distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE) do |hash, key|
62
+ hash[key] = []
63
+ end
64
+
65
+ @before_send = opts[:before_send]
58
66
  end
59
67
 
60
68
  # Synchronously waits until the worker has cleared the queue.
@@ -88,6 +96,10 @@ class PostHog
88
96
  # @option attrs [String] :event Event name
89
97
  # @option attrs [Hash] :properties Event properties (optional)
90
98
  # @option attrs [Bool] :send_feature_flags Whether to send feature flags with this event (optional)
99
+ # @option attrs [String] :uuid ID that uniquely identifies an event;
100
+ # events in PostHog are deduplicated by the
101
+ # combination of teamId, timestamp date,
102
+ # event name, distinct id, and UUID
91
103
  # @macro common_attrs
92
104
  def capture(attrs)
93
105
  symbolize_keys! attrs
@@ -147,18 +159,34 @@ class PostHog
147
159
  @queue.length
148
160
  end
149
161
 
150
- def is_feature_enabled(flag_key, distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false, send_feature_flag_events: true)
151
- response = get_feature_flag(flag_key, distinct_id, groups: groups, person_properties: person_properties, group_properties: group_properties, only_evaluate_locally: only_evaluate_locally, send_feature_flag_events: send_feature_flag_events)
152
- if response.nil?
153
- return nil
154
- end
162
+ # TODO: In future version, rename to `feature_flag_enabled?`
163
+ def is_feature_enabled( # rubocop:disable Naming/PredicateName
164
+ flag_key,
165
+ distinct_id,
166
+ groups: {},
167
+ person_properties: {},
168
+ group_properties: {},
169
+ only_evaluate_locally: false,
170
+ send_feature_flag_events: true
171
+ )
172
+ response = get_feature_flag(
173
+ flag_key,
174
+ distinct_id,
175
+ groups: groups,
176
+ person_properties: person_properties,
177
+ group_properties: group_properties,
178
+ only_evaluate_locally: only_evaluate_locally,
179
+ send_feature_flag_events: send_feature_flag_events
180
+ )
181
+ return nil if response.nil?
182
+
155
183
  !!response
156
184
  end
157
185
 
158
186
  # @param [String] flag_key The unique flag key of the feature flag
159
187
  # @return [String] The decrypted value of the feature flag payload
160
188
  def get_remote_config_payload(flag_key)
161
- return @feature_flags_poller.get_remote_config_payload(flag_key)
189
+ @feature_flags_poller.get_remote_config_payload(flag_key)
162
190
  end
163
191
 
164
192
  # Returns whether the given feature flag is enabled for the given user or not
@@ -168,35 +196,56 @@ class PostHog
168
196
  # @param [Hash] groups
169
197
  # @param [Hash] person_properties key-value pairs of properties to associate with the user.
170
198
  # @param [Hash] group_properties
171
- #
199
+ #
172
200
  # @return [String, nil] The value of the feature flag
173
201
  #
174
202
  # The provided properties are used to calculate feature flags locally, if possible.
175
203
  #
176
- # `groups` are a mapping from group type to group key. So, if you have a group type of "organization" and a group key of "5",
204
+ # `groups` are a mapping from group type to group key. So, if you have a group type of "organization"
205
+ # and a group key of "5",
177
206
  # you would pass groups={"organization": "5"}.
178
207
  # `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, and employee count,
180
- # you'll send these as:
208
+ # So, for example, if you have the group type "organization" and the group key "5", with the properties name,
209
+ # and employee count, you'll send these as:
181
210
  # ```ruby
182
211
  # group_properties: {"organization": {"name": "PostHog", "employees": 11}}
183
212
  # ```
184
- def get_feature_flag(key, distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false, send_feature_flag_events: true)
185
- person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties)
186
- feature_flag_response, flag_was_locally_evaluated, request_id = @feature_flags_poller.get_feature_flag(key, distinct_id, groups, person_properties, group_properties, only_evaluate_locally)
213
+ def get_feature_flag(
214
+ key,
215
+ distinct_id,
216
+ groups: {},
217
+ person_properties: {},
218
+ group_properties: {},
219
+ only_evaluate_locally: false,
220
+ send_feature_flag_events: true
221
+ )
222
+ person_properties, group_properties = add_local_person_and_group_properties(
223
+ distinct_id,
224
+ groups,
225
+ person_properties,
226
+ group_properties
227
+ )
228
+ feature_flag_response, flag_was_locally_evaluated, request_id = @feature_flags_poller.get_feature_flag(
229
+ key,
230
+ distinct_id,
231
+ groups,
232
+ person_properties,
233
+ group_properties,
234
+ only_evaluate_locally
235
+ )
187
236
 
188
237
  feature_flag_reported_key = "#{key}_#{feature_flag_response}"
189
238
  if !@distinct_id_has_sent_flag_calls[distinct_id].include?(feature_flag_reported_key) && send_feature_flag_events
190
239
  capture(
191
240
  {
192
- 'distinct_id': distinct_id,
193
- 'event': '$feature_flag_called',
194
- 'properties': {
241
+ distinct_id: distinct_id,
242
+ event: '$feature_flag_called',
243
+ properties: {
195
244
  '$feature_flag' => key,
196
245
  '$feature_flag_response' => feature_flag_response,
197
246
  'locally_evaluated' => flag_was_locally_evaluated
198
- }.merge(request_id ? {'$feature_flag_request_id' => request_id} : {}),
199
- 'groups': groups,
247
+ }.merge(request_id ? { '$feature_flag_request_id' => request_id } : {}),
248
+ groups: groups
200
249
  }
201
250
  )
202
251
  @distinct_id_has_sent_flag_calls[distinct_id] << feature_flag_reported_key
@@ -210,11 +259,19 @@ class PostHog
210
259
  # @param [Hash] groups
211
260
  # @param [Hash] person_properties key-value pairs of properties to associate with the user.
212
261
  # @param [Hash] group_properties
213
- #
262
+ #
214
263
  # @return [Hash] String (not symbol) key value pairs of flag and their values
215
- def get_all_flags(distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false)
216
- person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties)
217
- return @feature_flags_poller.get_all_flags(distinct_id, groups, person_properties, group_properties, only_evaluate_locally)
264
+ def get_all_flags(
265
+ distinct_id,
266
+ groups: {},
267
+ person_properties: {},
268
+ group_properties: {},
269
+ only_evaluate_locally: false
270
+ )
271
+ person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups,
272
+ person_properties, group_properties)
273
+ @feature_flags_poller.get_all_flags(distinct_id, groups, person_properties, group_properties,
274
+ only_evaluate_locally)
218
275
  end
219
276
 
220
277
  # Returns payload for a given feature flag
@@ -227,13 +284,23 @@ class PostHog
227
284
  # @option [Hash] group_properties
228
285
  # @option [Boolean] only_evaluate_locally
229
286
  #
230
- def get_feature_flag_payload(key, distinct_id, match_value: nil, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false)
231
- person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties)
232
- @feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties, group_properties, only_evaluate_locally)
287
+ def get_feature_flag_payload(
288
+ key,
289
+ distinct_id,
290
+ match_value: nil,
291
+ groups: {},
292
+ person_properties: {},
293
+ group_properties: {},
294
+ only_evaluate_locally: false
295
+ )
296
+ person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups,
297
+ person_properties, group_properties)
298
+ @feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties,
299
+ group_properties, only_evaluate_locally)
233
300
  end
234
301
 
235
302
  # Returns all flags and payloads for a given user
236
- #
303
+ #
237
304
  # @return [Hash] A hash with the following keys:
238
305
  # featureFlags: A hash of feature flags
239
306
  # featureFlagPayloads: A hash of feature flag payloads
@@ -244,9 +311,20 @@ class PostHog
244
311
  # @option [Hash] group_properties
245
312
  # @option [Boolean] only_evaluate_locally
246
313
  #
247
- def get_all_flags_and_payloads(distinct_id, groups: {}, person_properties: {}, group_properties: {}, only_evaluate_locally: false)
248
- person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties)
249
- response = @feature_flags_poller.get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties, only_evaluate_locally)
314
+ def get_all_flags_and_payloads(
315
+ distinct_id,
316
+ groups: {},
317
+ person_properties: {},
318
+ group_properties: {},
319
+ only_evaluate_locally: false
320
+ )
321
+ person_properties, group_properties = add_local_person_and_group_properties(
322
+ distinct_id, groups, person_properties, group_properties
323
+ )
324
+ response = @feature_flags_poller.get_all_flags_and_payloads(
325
+ distinct_id, groups, person_properties, group_properties, only_evaluate_locally
326
+ )
327
+
250
328
  response.delete(:requestId) # remove internal information.
251
329
  response
252
330
  end
@@ -268,10 +346,36 @@ class PostHog
268
346
 
269
347
  private
270
348
 
349
+ # before_send should run immediately before the event is sent to the queue.
350
+ # @param [Object] action The event to be sent to PostHog
351
+ # @return [null, Object, nil] The processed event or nil if the event should not be sent
352
+ def process_before_send(action)
353
+ return action if action.nil? || action.empty?
354
+ return action unless @before_send
355
+
356
+ begin
357
+ processed_action = @before_send.call(action)
358
+
359
+ if processed_action.nil?
360
+ logger.warn("Event #{action[:event]} was rejected in beforeSend function")
361
+ elsif processed_action.empty?
362
+ logger.warn("Event #{action[:event]} has no properties after beforeSend function, this is likely an error")
363
+ end
364
+
365
+ processed_action
366
+ rescue StandardError => e
367
+ logger.error("Error in beforeSend function - using original event: #{e.message}")
368
+ action
369
+ end
370
+ end
371
+
271
372
  # private: Enqueues the action.
272
373
  #
273
374
  # returns Boolean of whether the item was added to the queue.
274
375
  def enqueue(action)
376
+ action = process_before_send(action)
377
+ return false if action.nil? || action.empty?
378
+
275
379
  # add our request id for tracing purposes
276
380
  action[:messageId] ||= uid
277
381
 
@@ -283,8 +387,8 @@ class PostHog
283
387
  else
284
388
  logger.warn(
285
389
  'Queue is full, dropping events. The :max_queue_size ' \
286
- 'configuration parameter can be increased to prevent this from ' \
287
- 'happening.'
390
+ 'configuration parameter can be increased to prevent this from ' \
391
+ 'happening.'
288
392
  )
289
393
  false
290
394
  end
@@ -297,8 +401,10 @@ class PostHog
297
401
 
298
402
  def ensure_worker_running
299
403
  return if worker_running?
404
+
300
405
  @worker_mutex.synchronize do
301
406
  return if worker_running?
407
+
302
408
  @worker_thread = Thread.new { @worker.run }
303
409
  end
304
410
  end
@@ -308,7 +414,6 @@ class PostHog
308
414
  end
309
415
 
310
416
  def add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties)
311
-
312
417
  groups ||= {}
313
418
  person_properties ||= {}
314
419
  group_properties ||= {}
@@ -317,21 +422,22 @@ class PostHog
317
422
  symbolize_keys! person_properties
318
423
  symbolize_keys! group_properties
319
424
 
320
- group_properties.each do |key, value|
425
+ group_properties.each_value do |value|
321
426
  symbolize_keys! value
322
427
  end
323
428
 
324
- all_person_properties = { "distinct_id" => distinct_id }.merge(person_properties)
429
+ all_person_properties = { distinct_id: distinct_id }.merge(person_properties)
325
430
 
326
431
  all_group_properties = {}
327
432
  if groups
328
433
  groups.each do |group_name, group_key|
329
434
  all_group_properties[group_name] = {
330
- "$group_key" => group_key}.merge(group_properties&.dig(group_name) || {})
435
+ :'$group_key' => group_key
436
+ }.merge((group_properties && group_properties[group_name]) || {})
331
437
  end
332
438
  end
333
439
 
334
- return all_person_properties, all_group_properties
440
+ [all_person_properties, all_group_properties]
335
441
  end
336
442
  end
337
443
  end
@@ -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
@@ -4,34 +4,35 @@ class FeatureFlag
4
4
 
5
5
  def initialize(json)
6
6
  json.transform_keys!(&:to_s)
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
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
- def get_value
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&.payload
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
- "key" => key,
25
- "enabled" => value.is_a?(String) ? true : value,
26
- "variant" => value.is_a?(String) ? value : nil,
27
- "reason" => nil,
28
- "metadata" => {
29
- "id" => nil,
30
- "version" => nil,
31
- "payload" => payload,
32
- "description" => nil
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["code"]
45
- @description = json["description"]
46
- @condition_index = json["condition_index"]&.to_i if json["condition_index"]
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["id"]
57
- @version = json["version"]
58
- @payload = json["payload"]
59
- @description = json["description"]
57
+ @id = json['id']
58
+ @version = json['version']
59
+ @payload = json['payload']
60
+ @description = json['description']
60
61
  end
61
- end
62
+ end