posthog-ruby 3.3.3 → 3.4.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: 9a8d4402eef3a161a1c76c7e7e2349356741011cc6ead4acde7bd31ef437e568
4
- data.tar.gz: b9ad05adb1b5e3aea9849bf0bbceceef0d22f9724561e8f3a4a3e39f4ed104e8
3
+ metadata.gz: 9e99287bd9fe6eb8969c2481c0380c2a6eba12709de5f5189a053cb9ba5ca255
4
+ data.tar.gz: 78b496c5feb9c8cb222393ababa36d0f6fd81c18e603469b0633ac3fcdbfd639
5
5
  SHA512:
6
- metadata.gz: 055501a8e3ea62cfd321adee64297023338deb455ed59746632c0051179e3b3f6e733f1f7126eb612e5c381aa8bf8653b51ae2d15a1be938d80d05f8e0b06e3f
7
- data.tar.gz: c4dd9ae85bafbec60209c8aff9329f79585b99deb8ffe1624397df73d930d54c40ab5fefaac2de9b75adee80c242f98d26428b61f4155206ce7632517303241b
6
+ metadata.gz: dc1c52ff0243af441d895c05199067347931f0bf50a21ba897a3fb75640cb954ec66c79dddb29fb92338479014f3a7f42543a1ef651ebdcbd7fe6845ff9e9002
7
+ data.tar.gz: e46932963b504e54fe4590db73e1956611190b2ab16a1f04ffff5877c01b1e71a2dafc5f9c373361b69bb5f63532d1400c244580ac3c229ced006b350880eef5
@@ -285,26 +285,31 @@ module PostHog
285
285
  person_properties,
286
286
  group_properties
287
287
  )
288
- feature_flag_response, flag_was_locally_evaluated, request_id = @feature_flags_poller.get_feature_flag(
289
- key,
290
- distinct_id,
291
- groups,
292
- person_properties,
293
- group_properties,
294
- only_evaluate_locally
295
- )
288
+ feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at =
289
+ @feature_flags_poller.get_feature_flag(
290
+ key,
291
+ distinct_id,
292
+ groups,
293
+ person_properties,
294
+ group_properties,
295
+ only_evaluate_locally
296
+ )
296
297
 
297
298
  feature_flag_reported_key = "#{key}_#{feature_flag_response}"
298
299
  if !@distinct_id_has_sent_flag_calls[distinct_id].include?(feature_flag_reported_key) && send_feature_flag_events
300
+ properties = {
301
+ '$feature_flag' => key,
302
+ '$feature_flag_response' => feature_flag_response,
303
+ 'locally_evaluated' => flag_was_locally_evaluated
304
+ }
305
+ properties['$feature_flag_request_id'] = request_id if request_id
306
+ properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
307
+
299
308
  capture(
300
309
  {
301
310
  distinct_id: distinct_id,
302
311
  event: '$feature_flag_called',
303
- properties: {
304
- '$feature_flag' => key,
305
- '$feature_flag_response' => feature_flag_response,
306
- 'locally_evaluated' => flag_was_locally_evaluated
307
- }.merge(request_id ? { '$feature_flag_request_id' => request_id } : {}),
312
+ properties: properties,
308
313
  groups: groups
309
314
  }
310
315
  )
@@ -385,7 +390,9 @@ module PostHog
385
390
  distinct_id, groups, person_properties, group_properties, only_evaluate_locally
386
391
  )
387
392
 
388
- response.delete(:requestId) # remove internal information.
393
+ # Remove internal information
394
+ response.delete(:requestId)
395
+ response.delete(:evaluatedAt)
389
396
  response
390
397
  end
391
398
 
@@ -43,6 +43,7 @@ module PostHog
43
43
  @feature_flag_request_timeout_seconds = feature_flag_request_timeout_seconds
44
44
  @on_error = on_error || proc { |status, error| }
45
45
  @quota_limited = Concurrent::AtomicBoolean.new(false)
46
+ @flags_etag = Concurrent::AtomicReference.new(nil)
46
47
  @task =
47
48
  Concurrent::TimerTask.new(
48
49
  execution_interval: polling_interval
@@ -140,6 +141,7 @@ module PostHog
140
141
  [key, FeatureFlag.from_value_and_payload(key, value, flags_response[:featureFlagPayloads][key])]
141
142
  end
142
143
  end
144
+
143
145
  flags_response
144
146
  end
145
147
 
@@ -190,6 +192,7 @@ module PostHog
190
192
  flag_was_locally_evaluated = !response.nil?
191
193
 
192
194
  request_id = nil
195
+ evaluated_at = nil
193
196
 
194
197
  if !flag_was_locally_evaluated && !only_evaluate_locally
195
198
  begin
@@ -198,6 +201,7 @@ module PostHog
198
201
  if flags_data.key?(:featureFlags)
199
202
  flags = stringify_keys(flags_data[:featureFlags] || {})
200
203
  request_id = flags_data[:requestId]
204
+ evaluated_at = flags_data[:evaluatedAt]
201
205
  else
202
206
  logger.debug "Missing feature flags key: #{flags_data.to_json}"
203
207
  flags = {}
@@ -211,7 +215,7 @@ module PostHog
211
215
  end
212
216
  end
213
217
 
214
- [response, flag_was_locally_evaluated, request_id]
218
+ [response, flag_was_locally_evaluated, request_id, evaluated_at]
215
219
  end
216
220
 
217
221
  def get_all_flags(
@@ -252,6 +256,7 @@ module PostHog
252
256
  payloads = {}
253
257
  fallback_to_server = @feature_flags.empty?
254
258
  request_id = nil # Only for /flags requests
259
+ evaluated_at = nil # Only for /flags requests
255
260
 
256
261
  @feature_flags.each do |flag|
257
262
  match_value = _compute_flag_locally(flag, distinct_id, groups, person_properties, group_properties)
@@ -282,6 +287,7 @@ module PostHog
282
287
  flags = stringify_keys(flags_and_payloads[:featureFlags] || {})
283
288
  payloads = stringify_keys(flags_and_payloads[:featureFlagPayloads] || {})
284
289
  request_id = flags_and_payloads[:requestId]
290
+ evaluated_at = flags_and_payloads[:evaluatedAt]
285
291
  end
286
292
  rescue StandardError => e
287
293
  @on_error.call(-1, "Error computing flag remotely: #{e}")
@@ -292,7 +298,8 @@ module PostHog
292
298
  {
293
299
  featureFlags: flags,
294
300
  featureFlagPayloads: payloads,
295
- requestId: request_id
301
+ requestId: request_id,
302
+ evaluatedAt: evaluated_at
296
303
  }
297
304
  end
298
305
 
@@ -834,12 +841,20 @@ module PostHog
834
841
 
835
842
  def _load_feature_flags
836
843
  begin
837
- res = _request_feature_flag_definitions
844
+ res = _request_feature_flag_definitions(etag: @flags_etag.value)
838
845
  rescue StandardError => e
839
846
  @on_error.call(-1, e.to_s)
840
847
  return
841
848
  end
842
849
 
850
+ # Handle 304 Not Modified - flags haven't changed, skip processing
851
+ # Only update ETag if the 304 response includes one
852
+ if res[:not_modified]
853
+ @flags_etag.value = res[:etag] if res[:etag]
854
+ logger.debug '[FEATURE FLAGS] Flags not modified (304), using cached data'
855
+ return
856
+ end
857
+
843
858
  # Handle quota limits with 402 status
844
859
  if res.is_a?(Hash) && res[:status] == 402
845
860
  logger.warn(
@@ -856,6 +871,9 @@ module PostHog
856
871
  end
857
872
 
858
873
  if res.key?(:flags)
874
+ # Only update ETag on successful responses with flag data
875
+ @flags_etag.value = res[:etag]
876
+
859
877
  @feature_flags = res[:flags] || []
860
878
  @feature_flags_by_key = {}
861
879
  @feature_flags.each do |flag|
@@ -871,13 +889,14 @@ module PostHog
871
889
  end
872
890
  end
873
891
 
874
- def _request_feature_flag_definitions
892
+ def _request_feature_flag_definitions(etag: nil)
875
893
  uri = URI("#{@host}/api/feature_flag/local_evaluation")
876
894
  uri.query = URI.encode_www_form([['token', @project_api_key], %w[send_cohorts true]])
877
895
  req = Net::HTTP::Get.new(uri)
878
896
  req['Authorization'] = "Bearer #{@personal_api_key}"
897
+ req['If-None-Match'] = etag if etag
879
898
 
880
- _request(uri, req)
899
+ _request(uri, req, nil, include_etag: true)
881
900
  end
882
901
 
883
902
  def _request_feature_flag_evaluation(data = {})
@@ -901,7 +920,7 @@ module PostHog
901
920
  end
902
921
 
903
922
  # rubocop:disable Lint/ShadowedException
904
- def _request(uri, request_object, timeout = nil)
923
+ def _request(uri, request_object, timeout = nil, include_etag: false)
905
924
  request_object['User-Agent'] = "posthog-ruby#{PostHog::VERSION}"
906
925
  request_timeout = timeout || 10
907
926
 
@@ -913,16 +932,28 @@ module PostHog
913
932
  read_timeout: request_timeout
914
933
  ) do |http|
915
934
  res = http.request(request_object)
935
+ status_code = res.code.to_i
936
+ etag = include_etag ? res['ETag'] : nil
937
+
938
+ # Handle 304 Not Modified - return special response indicating no change
939
+ if status_code == 304
940
+ logger.debug("#{request_object.method} #{_mask_tokens_in_url(uri.to_s)} returned 304 Not Modified")
941
+ return { not_modified: true, etag: etag, status: status_code }
942
+ end
916
943
 
917
944
  # Parse response body to hash
918
945
  begin
919
946
  response = JSON.parse(res.body, { symbolize_names: true })
920
- # Only add status if response is a hash
921
- response = response.merge({ status: res.code.to_i }) if response.is_a?(Hash)
947
+ # Only add status (and etag if requested) if response is a hash
948
+ extra_fields = { status: status_code }
949
+ extra_fields[:etag] = etag if include_etag
950
+ response = response.merge(extra_fields) if response.is_a?(Hash)
922
951
  return response
923
952
  rescue JSON::ParserError
924
953
  # Handle case when response isn't valid JSON
925
- return { error: 'Invalid JSON response', body: res.body, status: res.code.to_i }
954
+ error_response = { error: 'Invalid JSON response', body: res.body, status: status_code }
955
+ error_response[:etag] = etag if include_etag
956
+ return error_response
926
957
  end
927
958
  end
928
959
  rescue Timeout::Error,
@@ -939,5 +970,9 @@ module PostHog
939
970
  end
940
971
  end
941
972
  # rubocop:enable Lint/ShadowedException
973
+
974
+ def _mask_tokens_in_url(url)
975
+ url.gsub(/token=([^&]{10})[^&]*/, 'token=\1...')
976
+ end
942
977
  end
943
978
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.3.3'
4
+ VERSION = '3.4.0'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: posthog-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.3
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-10-22 00:00:00.000000000 Z
10
+ date: 2025-12-04 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: concurrent-ruby