posthog-rails 3.5.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 +7 -0
- data/lib/posthog/backoff_policy.rb +46 -0
- data/lib/posthog/client.rb +545 -0
- data/lib/posthog/defaults.rb +44 -0
- data/lib/posthog/exception_capture.rb +116 -0
- data/lib/posthog/feature_flag.rb +66 -0
- data/lib/posthog/feature_flag_error.rb +36 -0
- data/lib/posthog/feature_flag_result.rb +56 -0
- data/lib/posthog/feature_flags.rb +1004 -0
- data/lib/posthog/field_parser.rb +194 -0
- data/lib/posthog/logging.rb +70 -0
- data/lib/posthog/message_batch.rb +73 -0
- data/lib/posthog/noop_worker.rb +19 -0
- data/lib/posthog/response.rb +15 -0
- data/lib/posthog/send_feature_flags_options.rb +34 -0
- data/lib/posthog/send_worker.rb +70 -0
- data/lib/posthog/transport.rb +144 -0
- data/lib/posthog/utils.rb +145 -0
- data/lib/posthog/version.rb +5 -0
- data/lib/posthog.rb +14 -0
- metadata +91 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c928e33ae64462619dd81b5b785402c17f4cc5ebba9d7858cb33d7b38b426d4a
|
|
4
|
+
data.tar.gz: 5ee1084fde161538e21b53f257bc485aa9fafc0b6b4549776a28ab5d56c2de7a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3e3cc76336d84ab77d948e5a0504694926565bec21f8b535d70e5eb6671449b0676e60677f9257170b5e84c703e351baf9fe35ee091544df26859fdba7210f4c
|
|
7
|
+
data.tar.gz: 54d4dd5aee80aa5f85961672934cb7c565e16342327678f9352ed7ed6e9d484479418585ef4e9cbf39eed6e00063290c2a9e253f50621796f8e50774c97a5ade
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'posthog/defaults'
|
|
4
|
+
|
|
5
|
+
module PostHog
|
|
6
|
+
class BackoffPolicy
|
|
7
|
+
include PostHog::Defaults::BackoffPolicy
|
|
8
|
+
|
|
9
|
+
# @param [Hash] opts
|
|
10
|
+
# @option opts [Numeric] :min_timeout_ms The minimum backoff timeout
|
|
11
|
+
# @option opts [Numeric] :max_timeout_ms The maximum backoff timeout
|
|
12
|
+
# @option opts [Numeric] :multiplier The value to multiply the current
|
|
13
|
+
# interval with for each retry attempt
|
|
14
|
+
# @option opts [Numeric] :randomization_factor The randomization factor
|
|
15
|
+
# to use to create a range around the retry interval
|
|
16
|
+
def initialize(opts = {})
|
|
17
|
+
@min_timeout_ms = opts[:min_timeout_ms] || MIN_TIMEOUT_MS
|
|
18
|
+
@max_timeout_ms = opts[:max_timeout_ms] || MAX_TIMEOUT_MS
|
|
19
|
+
@multiplier = opts[:multiplier] || MULTIPLIER
|
|
20
|
+
@randomization_factor =
|
|
21
|
+
opts[:randomization_factor] || RANDOMIZATION_FACTOR
|
|
22
|
+
|
|
23
|
+
@attempts = 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Numeric] the next backoff interval, in milliseconds.
|
|
27
|
+
def next_interval
|
|
28
|
+
interval = @min_timeout_ms * (@multiplier**@attempts)
|
|
29
|
+
interval = add_jitter(interval, @randomization_factor)
|
|
30
|
+
|
|
31
|
+
@attempts += 1
|
|
32
|
+
|
|
33
|
+
[interval, @max_timeout_ms].min
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def add_jitter(base, randomization_factor)
|
|
39
|
+
random_number = rand
|
|
40
|
+
max_deviation = base * randomization_factor
|
|
41
|
+
deviation = random_number * max_deviation
|
|
42
|
+
|
|
43
|
+
random_number < 0.5 ? base - deviation : base + deviation
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
require 'posthog/defaults'
|
|
7
|
+
require 'posthog/logging'
|
|
8
|
+
require 'posthog/utils'
|
|
9
|
+
require 'posthog/send_worker'
|
|
10
|
+
require 'posthog/noop_worker'
|
|
11
|
+
require 'posthog/feature_flags'
|
|
12
|
+
require 'posthog/send_feature_flags_options'
|
|
13
|
+
require 'posthog/exception_capture'
|
|
14
|
+
|
|
15
|
+
module PostHog
|
|
16
|
+
class Client
|
|
17
|
+
include PostHog::Utils
|
|
18
|
+
include PostHog::Logging
|
|
19
|
+
|
|
20
|
+
# @param [Hash] opts
|
|
21
|
+
# @option opts [String] :api_key Your project's api_key
|
|
22
|
+
# @option opts [String] :personal_api_key Your personal API key
|
|
23
|
+
# @option opts [FixNum] :max_queue_size Maximum number of calls to be
|
|
24
|
+
# remain queued. Defaults to 10_000.
|
|
25
|
+
# @option opts [Bool] :test_mode +true+ if messages should remain
|
|
26
|
+
# queued for testing. Defaults to +false+.
|
|
27
|
+
# @option opts [Proc] :on_error Handles error calls from the API.
|
|
28
|
+
# @option opts [String] :host Fully qualified hostname of the PostHog server. Defaults to `https://app.posthog.com`
|
|
29
|
+
# @option opts [Integer] :feature_flags_polling_interval How often to poll for feature flag definition changes.
|
|
30
|
+
# Measured in seconds, defaults to 30.
|
|
31
|
+
# @option opts [Integer] :feature_flag_request_timeout_seconds How long to wait for feature flag evaluation.
|
|
32
|
+
# Measured in seconds, defaults to 3.
|
|
33
|
+
# @option opts [Proc] :before_send A block that receives the event hash and should return either a modified hash
|
|
34
|
+
# to be sent to PostHog or nil to prevent the event from being sent. e.g. `before_send: ->(event) { event }`
|
|
35
|
+
def initialize(opts = {})
|
|
36
|
+
symbolize_keys!(opts)
|
|
37
|
+
|
|
38
|
+
opts[:host] ||= 'https://app.posthog.com'
|
|
39
|
+
|
|
40
|
+
@queue = Queue.new
|
|
41
|
+
@api_key = opts[:api_key]
|
|
42
|
+
@max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE
|
|
43
|
+
@worker_mutex = Mutex.new
|
|
44
|
+
@worker = if opts[:test_mode]
|
|
45
|
+
NoopWorker.new(@queue)
|
|
46
|
+
else
|
|
47
|
+
SendWorker.new(@queue, @api_key, opts)
|
|
48
|
+
end
|
|
49
|
+
@worker_thread = nil
|
|
50
|
+
@feature_flags_poller = nil
|
|
51
|
+
@personal_api_key = opts[:personal_api_key]
|
|
52
|
+
|
|
53
|
+
check_api_key!
|
|
54
|
+
|
|
55
|
+
@feature_flags_poller =
|
|
56
|
+
FeatureFlagsPoller.new(
|
|
57
|
+
opts[:feature_flags_polling_interval],
|
|
58
|
+
opts[:personal_api_key],
|
|
59
|
+
@api_key,
|
|
60
|
+
opts[:host],
|
|
61
|
+
opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS,
|
|
62
|
+
opts[:on_error]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@distinct_id_has_sent_flag_calls = SizeLimitedHash.new(Defaults::MAX_HASH_SIZE) do |hash, key|
|
|
66
|
+
hash[key] = []
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@before_send = opts[:before_send]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Synchronously waits until the worker has cleared the queue.
|
|
73
|
+
#
|
|
74
|
+
# Use only for scripts which are not long-running, and will specifically
|
|
75
|
+
# exit
|
|
76
|
+
def flush
|
|
77
|
+
while !@queue.empty? || @worker.is_requesting?
|
|
78
|
+
ensure_worker_running
|
|
79
|
+
sleep(0.1)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Clears the queue without waiting.
|
|
84
|
+
#
|
|
85
|
+
# Use only in test mode
|
|
86
|
+
def clear
|
|
87
|
+
@queue.clear
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @!macro common_attrs
|
|
91
|
+
# @option attrs [String] :message_id ID that uniquely
|
|
92
|
+
# identifies a message across the API. (optional)
|
|
93
|
+
# @option attrs [Time] :timestamp When the event occurred (optional)
|
|
94
|
+
# @option attrs [String] :distinct_id The ID for this user in your database
|
|
95
|
+
|
|
96
|
+
# Captures an event
|
|
97
|
+
#
|
|
98
|
+
# @param [Hash] attrs
|
|
99
|
+
#
|
|
100
|
+
# @option attrs [String] :event Event name
|
|
101
|
+
# @option attrs [Hash] :properties Event properties (optional)
|
|
102
|
+
# @option attrs [Bool, Hash, SendFeatureFlagsOptions] :send_feature_flags
|
|
103
|
+
# Whether to send feature flags with this event, or configuration for feature flag evaluation (optional)
|
|
104
|
+
# @option attrs [String] :uuid ID that uniquely identifies an event;
|
|
105
|
+
# events in PostHog are deduplicated by the
|
|
106
|
+
# combination of teamId, timestamp date,
|
|
107
|
+
# event name, distinct id, and UUID
|
|
108
|
+
# @macro common_attrs
|
|
109
|
+
def capture(attrs)
|
|
110
|
+
symbolize_keys! attrs
|
|
111
|
+
|
|
112
|
+
send_feature_flags_param = attrs[:send_feature_flags]
|
|
113
|
+
if send_feature_flags_param
|
|
114
|
+
# Handle different types of send_feature_flags parameter
|
|
115
|
+
case send_feature_flags_param
|
|
116
|
+
when true
|
|
117
|
+
# Backward compatibility: simple boolean
|
|
118
|
+
feature_variants = @feature_flags_poller.get_feature_variants(attrs[:distinct_id], attrs[:groups] || {})
|
|
119
|
+
when Hash
|
|
120
|
+
# Hash with options
|
|
121
|
+
options = SendFeatureFlagsOptions.from_hash(send_feature_flags_param)
|
|
122
|
+
feature_variants = @feature_flags_poller.get_feature_variants(
|
|
123
|
+
attrs[:distinct_id],
|
|
124
|
+
attrs[:groups] || {},
|
|
125
|
+
options ? options.person_properties : {},
|
|
126
|
+
options ? options.group_properties : {},
|
|
127
|
+
options ? options.only_evaluate_locally : false
|
|
128
|
+
)
|
|
129
|
+
when SendFeatureFlagsOptions
|
|
130
|
+
# SendFeatureFlagsOptions object
|
|
131
|
+
feature_variants = @feature_flags_poller.get_feature_variants(
|
|
132
|
+
attrs[:distinct_id],
|
|
133
|
+
attrs[:groups] || {},
|
|
134
|
+
send_feature_flags_param.person_properties,
|
|
135
|
+
send_feature_flags_param.group_properties,
|
|
136
|
+
send_feature_flags_param.only_evaluate_locally || false
|
|
137
|
+
)
|
|
138
|
+
else
|
|
139
|
+
# Invalid type, treat as false
|
|
140
|
+
feature_variants = nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
attrs[:feature_variants] = feature_variants if feature_variants
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
enqueue(FieldParser.parse_for_capture(attrs))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Captures an exception as an event
|
|
150
|
+
#
|
|
151
|
+
# @param [Exception, String, Object] exception The exception to capture, a string message, or exception-like object
|
|
152
|
+
# @param [String] distinct_id The ID for the user (optional, defaults to a generated UUID)
|
|
153
|
+
# @param [Hash] additional_properties Additional properties to include with the exception event (optional)
|
|
154
|
+
def capture_exception(exception, distinct_id = nil, additional_properties = {})
|
|
155
|
+
exception_info = ExceptionCapture.build_parsed_exception(exception)
|
|
156
|
+
|
|
157
|
+
return if exception_info.nil?
|
|
158
|
+
|
|
159
|
+
no_distinct_id_was_provided = distinct_id.nil?
|
|
160
|
+
distinct_id ||= SecureRandom.uuid
|
|
161
|
+
|
|
162
|
+
properties = { '$exception_list' => [exception_info] }
|
|
163
|
+
properties.merge!(additional_properties) if additional_properties && !additional_properties.empty?
|
|
164
|
+
properties['$process_person_profile'] = false if no_distinct_id_was_provided
|
|
165
|
+
|
|
166
|
+
event_data = {
|
|
167
|
+
distinct_id: distinct_id,
|
|
168
|
+
event: '$exception',
|
|
169
|
+
properties: properties,
|
|
170
|
+
timestamp: Time.now
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
capture(event_data)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Identifies a user
|
|
177
|
+
#
|
|
178
|
+
# @param [Hash] attrs
|
|
179
|
+
#
|
|
180
|
+
# @option attrs [Hash] :properties User properties (optional)
|
|
181
|
+
# @macro common_attrs
|
|
182
|
+
def identify(attrs)
|
|
183
|
+
symbolize_keys! attrs
|
|
184
|
+
enqueue(FieldParser.parse_for_identify(attrs))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Identifies a group
|
|
188
|
+
#
|
|
189
|
+
# @param [Hash] attrs
|
|
190
|
+
#
|
|
191
|
+
# @option attrs [String] :group_type Group type
|
|
192
|
+
# @option attrs [String] :group_key Group key
|
|
193
|
+
# @option attrs [Hash] :properties Group properties (optional)
|
|
194
|
+
# @option attrs [String] :distinct_id Distinct ID (optional)
|
|
195
|
+
# @macro common_attrs
|
|
196
|
+
def group_identify(attrs)
|
|
197
|
+
symbolize_keys! attrs
|
|
198
|
+
enqueue(FieldParser.parse_for_group_identify(attrs))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Aliases a user from one id to another
|
|
202
|
+
#
|
|
203
|
+
# @param [Hash] attrs
|
|
204
|
+
#
|
|
205
|
+
# @option attrs [String] :alias The alias to give the distinct id
|
|
206
|
+
# @macro common_attrs
|
|
207
|
+
def alias(attrs)
|
|
208
|
+
symbolize_keys! attrs
|
|
209
|
+
enqueue(FieldParser.parse_for_alias(attrs))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# @return [Hash] pops the last message from the queue
|
|
213
|
+
def dequeue_last_message
|
|
214
|
+
@queue.pop
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @return [Fixnum] number of messages in the queue
|
|
218
|
+
def queued_messages
|
|
219
|
+
@queue.length
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# TODO: In future version, rename to `feature_flag_enabled?`
|
|
223
|
+
def is_feature_enabled( # rubocop:disable Naming/PredicateName
|
|
224
|
+
flag_key,
|
|
225
|
+
distinct_id,
|
|
226
|
+
groups: {},
|
|
227
|
+
person_properties: {},
|
|
228
|
+
group_properties: {},
|
|
229
|
+
only_evaluate_locally: false,
|
|
230
|
+
send_feature_flag_events: true
|
|
231
|
+
)
|
|
232
|
+
response = get_feature_flag(
|
|
233
|
+
flag_key,
|
|
234
|
+
distinct_id,
|
|
235
|
+
groups: groups,
|
|
236
|
+
person_properties: person_properties,
|
|
237
|
+
group_properties: group_properties,
|
|
238
|
+
only_evaluate_locally: only_evaluate_locally,
|
|
239
|
+
send_feature_flag_events: send_feature_flag_events
|
|
240
|
+
)
|
|
241
|
+
return nil if response.nil?
|
|
242
|
+
|
|
243
|
+
!!response
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# @param [String] flag_key The unique flag key of the feature flag
|
|
247
|
+
# @return [String] The decrypted value of the feature flag payload
|
|
248
|
+
def get_remote_config_payload(flag_key)
|
|
249
|
+
@feature_flags_poller.get_remote_config_payload(flag_key)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Returns whether the given feature flag is enabled for the given user or not
|
|
253
|
+
#
|
|
254
|
+
# @param [String] key The key of the feature flag
|
|
255
|
+
# @param [String] distinct_id The distinct id of the user
|
|
256
|
+
# @param [Hash] groups
|
|
257
|
+
# @param [Hash] person_properties key-value pairs of properties to associate with the user.
|
|
258
|
+
# @param [Hash] group_properties
|
|
259
|
+
#
|
|
260
|
+
# @return [String, nil] The value of the feature flag
|
|
261
|
+
#
|
|
262
|
+
# The provided properties are used to calculate feature flags locally, if possible.
|
|
263
|
+
#
|
|
264
|
+
# `groups` are a mapping from group type to group key. So, if you have a group type of "organization"
|
|
265
|
+
# and a group key of "5",
|
|
266
|
+
# you would pass groups={"organization": "5"}.
|
|
267
|
+
# `group_properties` take the format: { group_type_name: { group_properties } }
|
|
268
|
+
# So, for example, if you have the group type "organization" and the group key "5", with the properties name,
|
|
269
|
+
# and employee count, you'll send these as:
|
|
270
|
+
# ```ruby
|
|
271
|
+
# group_properties: {"organization": {"name": "PostHog", "employees": 11}}
|
|
272
|
+
# ```
|
|
273
|
+
def get_feature_flag(
|
|
274
|
+
key,
|
|
275
|
+
distinct_id,
|
|
276
|
+
groups: {},
|
|
277
|
+
person_properties: {},
|
|
278
|
+
group_properties: {},
|
|
279
|
+
only_evaluate_locally: false,
|
|
280
|
+
send_feature_flag_events: true
|
|
281
|
+
)
|
|
282
|
+
result = get_feature_flag_result(
|
|
283
|
+
key,
|
|
284
|
+
distinct_id,
|
|
285
|
+
groups: groups,
|
|
286
|
+
person_properties: person_properties,
|
|
287
|
+
group_properties: group_properties,
|
|
288
|
+
only_evaluate_locally: only_evaluate_locally,
|
|
289
|
+
send_feature_flag_events: send_feature_flag_events
|
|
290
|
+
)
|
|
291
|
+
result&.value
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Returns both the feature flag value and payload in a single call.
|
|
295
|
+
# This method raises the $feature_flag_called event with the payload included.
|
|
296
|
+
#
|
|
297
|
+
# @param [String] key The key of the feature flag
|
|
298
|
+
# @param [String] distinct_id The distinct id of the user
|
|
299
|
+
# @param [Hash] groups
|
|
300
|
+
# @param [Hash] person_properties key-value pairs of properties to associate with the user.
|
|
301
|
+
# @param [Hash] group_properties
|
|
302
|
+
# @param [Boolean] only_evaluate_locally
|
|
303
|
+
# @param [Boolean] send_feature_flag_events
|
|
304
|
+
#
|
|
305
|
+
# @return [FeatureFlagResult, nil] A FeatureFlagResult object containing the flag value and payload,
|
|
306
|
+
# or nil if the flag evaluation returned nil
|
|
307
|
+
def get_feature_flag_result(
|
|
308
|
+
key,
|
|
309
|
+
distinct_id,
|
|
310
|
+
groups: {},
|
|
311
|
+
person_properties: {},
|
|
312
|
+
group_properties: {},
|
|
313
|
+
only_evaluate_locally: false,
|
|
314
|
+
send_feature_flag_events: true
|
|
315
|
+
)
|
|
316
|
+
person_properties, group_properties = add_local_person_and_group_properties(
|
|
317
|
+
distinct_id,
|
|
318
|
+
groups,
|
|
319
|
+
person_properties,
|
|
320
|
+
group_properties
|
|
321
|
+
)
|
|
322
|
+
feature_flag_response, flag_was_locally_evaluated, request_id, evaluated_at, feature_flag_error, payload =
|
|
323
|
+
@feature_flags_poller.get_feature_flag(
|
|
324
|
+
key,
|
|
325
|
+
distinct_id,
|
|
326
|
+
groups,
|
|
327
|
+
person_properties,
|
|
328
|
+
group_properties,
|
|
329
|
+
only_evaluate_locally
|
|
330
|
+
)
|
|
331
|
+
feature_flag_reported_key = "#{key}_#{feature_flag_response}"
|
|
332
|
+
|
|
333
|
+
if !@distinct_id_has_sent_flag_calls[distinct_id].include?(feature_flag_reported_key) && send_feature_flag_events
|
|
334
|
+
properties = {
|
|
335
|
+
'$feature_flag' => key,
|
|
336
|
+
'$feature_flag_response' => feature_flag_response,
|
|
337
|
+
'locally_evaluated' => flag_was_locally_evaluated
|
|
338
|
+
}
|
|
339
|
+
properties['$feature_flag_request_id'] = request_id if request_id
|
|
340
|
+
properties['$feature_flag_evaluated_at'] = evaluated_at if evaluated_at
|
|
341
|
+
properties['$feature_flag_error'] = feature_flag_error if feature_flag_error
|
|
342
|
+
|
|
343
|
+
capture(
|
|
344
|
+
distinct_id: distinct_id,
|
|
345
|
+
event: '$feature_flag_called',
|
|
346
|
+
properties: properties,
|
|
347
|
+
groups: groups
|
|
348
|
+
)
|
|
349
|
+
@distinct_id_has_sent_flag_calls[distinct_id] << feature_flag_reported_key
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
FeatureFlagResult.from_value_and_payload(key, feature_flag_response, payload)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Returns all flags for a given user
|
|
356
|
+
#
|
|
357
|
+
# @param [String] distinct_id The distinct id of the user
|
|
358
|
+
# @param [Hash] groups
|
|
359
|
+
# @param [Hash] person_properties key-value pairs of properties to associate with the user.
|
|
360
|
+
# @param [Hash] group_properties
|
|
361
|
+
#
|
|
362
|
+
# @return [Hash] String (not symbol) key value pairs of flag and their values
|
|
363
|
+
def get_all_flags(
|
|
364
|
+
distinct_id,
|
|
365
|
+
groups: {},
|
|
366
|
+
person_properties: {},
|
|
367
|
+
group_properties: {},
|
|
368
|
+
only_evaluate_locally: false
|
|
369
|
+
)
|
|
370
|
+
person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups,
|
|
371
|
+
person_properties, group_properties)
|
|
372
|
+
@feature_flags_poller.get_all_flags(distinct_id, groups, person_properties, group_properties,
|
|
373
|
+
only_evaluate_locally)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Returns payload for a given feature flag
|
|
377
|
+
#
|
|
378
|
+
# @deprecated Use {#get_feature_flag_result} instead, which returns both the flag value and payload
|
|
379
|
+
# and properly raises the $feature_flag_called event.
|
|
380
|
+
#
|
|
381
|
+
# @param [String] key The key of the feature flag
|
|
382
|
+
# @param [String] distinct_id The distinct id of the user
|
|
383
|
+
# @option [String or boolean] match_value The value of the feature flag to be matched
|
|
384
|
+
# @option [Hash] groups
|
|
385
|
+
# @option [Hash] person_properties key-value pairs of properties to associate with the user.
|
|
386
|
+
# @option [Hash] group_properties
|
|
387
|
+
# @option [Boolean] only_evaluate_locally
|
|
388
|
+
#
|
|
389
|
+
def get_feature_flag_payload(
|
|
390
|
+
key,
|
|
391
|
+
distinct_id,
|
|
392
|
+
match_value: nil,
|
|
393
|
+
groups: {},
|
|
394
|
+
person_properties: {},
|
|
395
|
+
group_properties: {},
|
|
396
|
+
only_evaluate_locally: false
|
|
397
|
+
)
|
|
398
|
+
person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups,
|
|
399
|
+
person_properties, group_properties)
|
|
400
|
+
@feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties,
|
|
401
|
+
group_properties, only_evaluate_locally)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Returns all flags and payloads for a given user
|
|
405
|
+
#
|
|
406
|
+
# @return [Hash] A hash with the following keys:
|
|
407
|
+
# featureFlags: A hash of feature flags
|
|
408
|
+
# featureFlagPayloads: A hash of feature flag payloads
|
|
409
|
+
#
|
|
410
|
+
# @param [String] distinct_id The distinct id of the user
|
|
411
|
+
# @option [Hash] groups
|
|
412
|
+
# @option [Hash] person_properties key-value pairs of properties to associate with the user.
|
|
413
|
+
# @option [Hash] group_properties
|
|
414
|
+
# @option [Boolean] only_evaluate_locally
|
|
415
|
+
#
|
|
416
|
+
def get_all_flags_and_payloads(
|
|
417
|
+
distinct_id,
|
|
418
|
+
groups: {},
|
|
419
|
+
person_properties: {},
|
|
420
|
+
group_properties: {},
|
|
421
|
+
only_evaluate_locally: false
|
|
422
|
+
)
|
|
423
|
+
person_properties, group_properties = add_local_person_and_group_properties(
|
|
424
|
+
distinct_id, groups, person_properties, group_properties
|
|
425
|
+
)
|
|
426
|
+
response = @feature_flags_poller.get_all_flags_and_payloads(
|
|
427
|
+
distinct_id, groups, person_properties, group_properties, only_evaluate_locally
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Remove internal information
|
|
431
|
+
response.delete(:requestId)
|
|
432
|
+
response.delete(:evaluatedAt)
|
|
433
|
+
response
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def reload_feature_flags
|
|
437
|
+
unless @personal_api_key
|
|
438
|
+
logger.error(
|
|
439
|
+
'You need to specify a personal_api_key to locally evaluate feature flags'
|
|
440
|
+
)
|
|
441
|
+
return
|
|
442
|
+
end
|
|
443
|
+
@feature_flags_poller.load_feature_flags(true)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def shutdown
|
|
447
|
+
@feature_flags_poller.shutdown_poller
|
|
448
|
+
flush
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
private
|
|
452
|
+
|
|
453
|
+
# before_send should run immediately before the event is sent to the queue.
|
|
454
|
+
# @param [Object] action The event to be sent to PostHog
|
|
455
|
+
# @return [null, Object, nil] The processed event or nil if the event should not be sent
|
|
456
|
+
def process_before_send(action)
|
|
457
|
+
return action if action.nil? || action.empty?
|
|
458
|
+
return action unless @before_send
|
|
459
|
+
|
|
460
|
+
begin
|
|
461
|
+
processed_action = @before_send.call(action)
|
|
462
|
+
|
|
463
|
+
if processed_action.nil?
|
|
464
|
+
logger.warn("Event #{action[:event]} was rejected in beforeSend function")
|
|
465
|
+
elsif processed_action.empty?
|
|
466
|
+
logger.warn("Event #{action[:event]} has no properties after beforeSend function, this is likely an error")
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
processed_action
|
|
470
|
+
rescue StandardError => e
|
|
471
|
+
logger.error("Error in beforeSend function - using original event: #{e.message}")
|
|
472
|
+
action
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# private: Enqueues the action.
|
|
477
|
+
#
|
|
478
|
+
# returns Boolean of whether the item was added to the queue.
|
|
479
|
+
def enqueue(action)
|
|
480
|
+
action = process_before_send(action)
|
|
481
|
+
return false if action.nil? || action.empty?
|
|
482
|
+
|
|
483
|
+
# add our request id for tracing purposes
|
|
484
|
+
action[:messageId] ||= uid
|
|
485
|
+
|
|
486
|
+
if @queue.length < @max_queue_size
|
|
487
|
+
@queue << action
|
|
488
|
+
ensure_worker_running
|
|
489
|
+
|
|
490
|
+
true
|
|
491
|
+
else
|
|
492
|
+
logger.warn(
|
|
493
|
+
'Queue is full, dropping events. The :max_queue_size ' \
|
|
494
|
+
'configuration parameter can be increased to prevent this from ' \
|
|
495
|
+
'happening.'
|
|
496
|
+
)
|
|
497
|
+
false
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# private: Checks that the api_key is properly initialized
|
|
502
|
+
def check_api_key!
|
|
503
|
+
raise ArgumentError, 'API key must be initialized' if @api_key.nil?
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def ensure_worker_running
|
|
507
|
+
return if worker_running?
|
|
508
|
+
|
|
509
|
+
@worker_mutex.synchronize do
|
|
510
|
+
return if worker_running?
|
|
511
|
+
|
|
512
|
+
@worker_thread = Thread.new { @worker.run }
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def worker_running?
|
|
517
|
+
@worker_thread&.alive?
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties)
|
|
521
|
+
groups ||= {}
|
|
522
|
+
person_properties ||= {}
|
|
523
|
+
group_properties ||= {}
|
|
524
|
+
|
|
525
|
+
symbolize_keys! groups
|
|
526
|
+
symbolize_keys! person_properties
|
|
527
|
+
symbolize_keys! group_properties
|
|
528
|
+
|
|
529
|
+
group_properties.each_value do |value|
|
|
530
|
+
symbolize_keys! value
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
all_person_properties = { distinct_id: distinct_id }.merge(person_properties)
|
|
534
|
+
|
|
535
|
+
all_group_properties = {}
|
|
536
|
+
groups&.each do |group_name, group_key|
|
|
537
|
+
all_group_properties[group_name] = {
|
|
538
|
+
'$group_key': group_key
|
|
539
|
+
}.merge((group_properties && group_properties[group_name]) || {})
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
[all_person_properties, all_group_properties]
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostHog
|
|
4
|
+
module Defaults
|
|
5
|
+
MAX_HASH_SIZE = 50_000
|
|
6
|
+
|
|
7
|
+
module Request
|
|
8
|
+
HOST = 'app.posthog.com'
|
|
9
|
+
PORT = 443
|
|
10
|
+
PATH = '/batch/'
|
|
11
|
+
SSL = true
|
|
12
|
+
HEADERS = {
|
|
13
|
+
'Accept' => 'application/json',
|
|
14
|
+
'Content-Type' => 'application/json',
|
|
15
|
+
'User-Agent' => "posthog-ruby/#{PostHog::VERSION}"
|
|
16
|
+
}.freeze
|
|
17
|
+
RETRIES = 10
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module FeatureFlags
|
|
21
|
+
FLAG_REQUEST_TIMEOUT_SECONDS = 3
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module Queue
|
|
25
|
+
MAX_SIZE = 10_000
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module Message
|
|
29
|
+
MAX_BYTES = 32_768 # 32Kb
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module MessageBatch
|
|
33
|
+
MAX_BYTES = 512_000 # 500Kb
|
|
34
|
+
MAX_SIZE = 100
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module BackoffPolicy
|
|
38
|
+
MIN_TIMEOUT_MS = 100
|
|
39
|
+
MAX_TIMEOUT_MS = 10_000
|
|
40
|
+
MULTIPLIER = 1.5
|
|
41
|
+
RANDOMIZATION_FACTOR = 0.5
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|