ablaevent-ruby 2.3.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.
@@ -0,0 +1,466 @@
1
+ require 'concurrent'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'posthog/version'
5
+ require 'posthog/logging'
6
+ require 'digest'
7
+
8
+ class PostHog
9
+
10
+ class InconclusiveMatchError < StandardError
11
+ end
12
+
13
+ class FeatureFlagsPoller
14
+ include PostHog::Logging
15
+ include PostHog::Utils
16
+
17
+ def initialize(polling_interval, personal_api_key, project_api_key, host)
18
+ @polling_interval = polling_interval || 30
19
+ @personal_api_key = personal_api_key
20
+ @project_api_key = project_api_key
21
+ @host = host
22
+ @feature_flags = Concurrent::Array.new
23
+ @group_type_mapping = Concurrent::Hash.new
24
+ @loaded_flags_successfully_once = Concurrent::AtomicBoolean.new
25
+ @feature_flags_by_key = nil
26
+
27
+ @task =
28
+ Concurrent::TimerTask.new(
29
+ execution_interval: polling_interval,
30
+ ) { _load_feature_flags }
31
+
32
+ # If no personal API key, disable local evaluation & thus polling for definitions
33
+ if @personal_api_key.nil?
34
+ logger.info "No personal API key provided, disabling local evaluation"
35
+ @loaded_flags_successfully_once.make_true
36
+ else
37
+ # load once before timer
38
+ load_feature_flags
39
+ @task.execute
40
+ end
41
+ end
42
+
43
+ def load_feature_flags(force_reload = false)
44
+ if @loaded_flags_successfully_once.false? || force_reload
45
+ _load_feature_flags
46
+ end
47
+ end
48
+
49
+ def get_feature_variants(distinct_id, groups={}, person_properties={}, group_properties={})
50
+ decide_data = get_decide(distinct_id, groups, person_properties, group_properties)
51
+ if !decide_data.key?(:featureFlags)
52
+ logger.error "Missing feature flags key: #{decide_data.to_json}"
53
+ else
54
+ stringify_keys(decide_data[:featureFlags] || {})
55
+ end
56
+ end
57
+
58
+ def _get_active_feature_variants(distinct_id, groups={}, person_properties={}, group_properties={})
59
+ feature_variants = get_feature_variants(distinct_id, groups, person_properties, group_properties)
60
+ active_feature_variants = {}
61
+ feature_variants.each do |key, value|
62
+ if value != false
63
+ active_feature_variants[key] = value
64
+ end
65
+ end
66
+ active_feature_variants
67
+ end
68
+
69
+ def get_feature_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false)
70
+ decide_data = get_decide(distinct_id, groups, person_properties, group_properties)
71
+ if !decide_data.key?(:featureFlagPayloads)
72
+ logger.error "Missing feature flag payloads key: #{decide_data.to_json}"
73
+ else
74
+ stringify_keys(decide_data[:featureFlagPayloads] || {})
75
+ end
76
+ end
77
+
78
+ def get_decide(distinct_id, groups={}, person_properties={}, group_properties={})
79
+ request_data = {
80
+ "distinct_id": distinct_id,
81
+ "groups": groups,
82
+ "person_properties": person_properties,
83
+ "group_properties": group_properties,
84
+ }
85
+
86
+ decide_data = _request_feature_flag_evaluation(request_data)
87
+ end
88
+
89
+ def get_feature_flag(key, distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false)
90
+ # make sure they're loaded on first run
91
+ load_feature_flags
92
+
93
+ symbolize_keys! groups
94
+ symbolize_keys! person_properties
95
+ symbolize_keys! group_properties
96
+
97
+ group_properties.each do |key, value|
98
+ symbolize_keys! value
99
+ end
100
+
101
+ response = nil
102
+ feature_flag = nil
103
+
104
+ @feature_flags.each do |flag|
105
+ if key == flag[:key]
106
+ feature_flag = flag
107
+ break
108
+ end
109
+ end
110
+
111
+ if !feature_flag.nil?
112
+ begin
113
+ response = _compute_flag_locally(feature_flag, distinct_id, groups, person_properties, group_properties)
114
+ logger.debug "Successfully computed flag locally: #{key} -> #{response}"
115
+ rescue InconclusiveMatchError => e
116
+ logger.debug "Failed to compute flag #{key} locally: #{e}"
117
+ rescue StandardError => e
118
+ logger.error "Error computing flag locally: #{e}. #{e.backtrace.join("\n")}"
119
+ end
120
+ end
121
+
122
+ flag_was_locally_evaluated = !response.nil?
123
+
124
+ if !flag_was_locally_evaluated && !only_evaluate_locally
125
+ begin
126
+ flags = get_feature_variants(distinct_id, groups, person_properties, group_properties)
127
+ response = flags[key]
128
+ if response.nil?
129
+ response = false
130
+ end
131
+ logger.debug "Successfully computed flag remotely: #{key} -> #{response}"
132
+ rescue StandardError => e
133
+ logger.error "Error computing flag remotely: #{e}. #{e.backtrace.join("\n")}"
134
+ end
135
+ end
136
+
137
+ [response, flag_was_locally_evaluated]
138
+ end
139
+
140
+ def get_all_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false)
141
+ # returns a string hash of all flags
142
+ response = get_all_flags_and_payloads(distinct_id, groups, person_properties, group_properties, only_evaluate_locally)
143
+ flags = response[:featureFlags]
144
+ end
145
+
146
+ def get_all_flags_and_payloads(distinct_id, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false)
147
+ load_feature_flags
148
+
149
+ flags = {}
150
+ payloads = {}
151
+ fallback_to_decide = @feature_flags.empty?
152
+
153
+ @feature_flags.each do |flag|
154
+ begin
155
+ match_value = _compute_flag_locally(flag, distinct_id, groups, person_properties, group_properties)
156
+ flags[flag[:key]] = match_value
157
+ match_payload = _compute_flag_payload_locally(flag[:key], match_value)
158
+ if match_payload
159
+ payloads[flag[:key]] = match_payload
160
+ end
161
+ rescue InconclusiveMatchError => e
162
+ fallback_to_decide = true
163
+ rescue StandardError => e
164
+ logger.error "Error computing flag locally: #{e}."
165
+ fallback_to_decide = true
166
+ end
167
+ end
168
+ if fallback_to_decide && !only_evaluate_locally
169
+ begin
170
+ flags_and_payloads = get_decide(distinct_id, groups, person_properties, group_properties)
171
+ flags = stringify_keys(flags_and_payloads[:featureFlags] || {})
172
+ payloads = stringify_keys(flags_and_payloads[:featureFlagPayloads] || {})
173
+ rescue StandardError => e
174
+ logger.error "Error computing flag remotely: #{e}"
175
+ end
176
+ end
177
+ {"featureFlags": flags, "featureFlagPayloads": payloads}
178
+ end
179
+
180
+ def get_feature_flag_payload(key, distinct_id, match_value = nil, groups = {}, person_properties = {}, group_properties = {}, only_evaluate_locally = false)
181
+ if match_value == nil
182
+ match_value = get_feature_flag(
183
+ key,
184
+ distinct_id,
185
+ groups,
186
+ person_properties,
187
+ group_properties,
188
+ true,
189
+ )[0]
190
+ end
191
+ response = nil
192
+ if match_value != nil
193
+ response = _compute_flag_payload_locally(key, match_value)
194
+ end
195
+ if response == nil and !only_evaluate_locally
196
+ decide_payloads = get_feature_payloads(distinct_id, groups, person_properties, group_properties)
197
+ response = decide_payloads[key.downcase] || nil
198
+ end
199
+ response
200
+ end
201
+
202
+ def shutdown_poller()
203
+ @task.shutdown
204
+ end
205
+
206
+ # Class methods
207
+
208
+ def self.match_property(property, property_values)
209
+ # only looks for matches where key exists in property_values
210
+ # doesn't support operator is_not_set
211
+
212
+ PostHog::Utils.symbolize_keys! property
213
+ PostHog::Utils.symbolize_keys! property_values
214
+
215
+ key = property[:key].to_sym
216
+ value = property[:value]
217
+ operator = property[:operator] || 'exact'
218
+
219
+ if !property_values.key?(key)
220
+ raise InconclusiveMatchError.new("Property #{key} not found in property_values")
221
+ elsif operator == 'is_not_set'
222
+ raise InconclusiveMatchError.new("Operator is_not_set not supported")
223
+ end
224
+
225
+ override_value = property_values[key]
226
+
227
+ case operator
228
+ when 'exact'
229
+ value.is_a?(Array) ? value.include?(override_value) : value == override_value
230
+ when 'is_not'
231
+ value.is_a?(Array) ? !value.include?(override_value) : value != override_value
232
+ when'is_set'
233
+ property_values.key?(key)
234
+ when 'icontains'
235
+ override_value.to_s.downcase.include?(value.to_s.downcase)
236
+ when 'not_icontains'
237
+ !override_value.to_s.downcase.include?(value.to_s.downcase)
238
+ when 'regex'
239
+ PostHog::Utils.is_valid_regex(value.to_s) && !Regexp.new(value.to_s).match(override_value.to_s).nil?
240
+ when 'not_regex'
241
+ PostHog::Utils.is_valid_regex(value.to_s) && Regexp.new(value.to_s).match(override_value.to_s).nil?
242
+ when 'gt'
243
+ override_value.class == value.class && override_value > value
244
+ when 'gte'
245
+ override_value.class == value.class && override_value >= value
246
+ when 'lt'
247
+ override_value.class == value.class && override_value < value
248
+ when 'lte'
249
+ override_value.class == value.class && override_value <= value
250
+ when 'is_date_before', 'is_date_after'
251
+ parsed_date = PostHog::Utils::convert_to_datetime(value)
252
+ override_date = PostHog::Utils::convert_to_datetime(override_value)
253
+ if operator == 'is_date_before'
254
+ return override_date < parsed_date
255
+ else
256
+ return override_date > parsed_date
257
+ end
258
+ else
259
+ logger.error "Unknown operator: #{operator}"
260
+ false
261
+ end
262
+ end
263
+
264
+ private
265
+
266
+ def _compute_flag_locally(flag, distinct_id, groups = {}, person_properties = {}, group_properties = {})
267
+ if flag[:ensure_experience_continuity]
268
+ raise InconclusiveMatchError.new("Flag has experience continuity enabled")
269
+ end
270
+
271
+ return false if !flag[:active]
272
+
273
+ flag_filters = flag[:filters] || {}
274
+
275
+ aggregation_group_type_index = flag_filters[:aggregation_group_type_index]
276
+ if !aggregation_group_type_index.nil?
277
+ group_name = @group_type_mapping[aggregation_group_type_index.to_s.to_sym]
278
+
279
+ if group_name.nil?
280
+ logger.warn "[FEATURE FLAGS] Unknown group type index #{aggregation_group_type_index} for feature flag #{flag[:key]}"
281
+ # failover to `/decide/`
282
+ raise InconclusiveMatchError.new("Flag has unknown group type index")
283
+ end
284
+
285
+ group_name_symbol = group_name.to_sym
286
+
287
+ if !groups.key?(group_name_symbol)
288
+ # Group flags are never enabled if appropriate `groups` aren't passed in
289
+ # don't failover to `/decide/`, since response will be the same
290
+ logger.warn "[FEATURE FLAGS] Can't compute group feature flag: #{flag[:key]} without group names passed in"
291
+ return false
292
+ end
293
+
294
+ focused_group_properties = group_properties[group_name_symbol]
295
+ return match_feature_flag_properties(flag, groups[group_name_symbol], focused_group_properties)
296
+ else
297
+ return match_feature_flag_properties(flag, distinct_id, person_properties)
298
+ end
299
+
300
+ end
301
+
302
+ def _compute_flag_payload_locally(key, match_value)
303
+ response = nil
304
+
305
+ if [true, false].include? match_value
306
+ response = @feature_flags_by_key.dig(key, :filters, :payloads, match_value.to_s.to_sym)
307
+ elsif match_value.is_a? String
308
+ response = @feature_flags_by_key.dig(key, :filters, :payloads, match_value.to_sym)
309
+ end
310
+ response
311
+ end
312
+
313
+ def match_feature_flag_properties(flag, distinct_id, properties)
314
+ flag_filters = flag[:filters] || {}
315
+
316
+ flag_conditions = flag_filters[:groups] || []
317
+ is_inconclusive = false
318
+ result = nil
319
+
320
+ # Stable sort conditions with variant overrides to the top. This ensures that if overrides are present, they are
321
+ # evaluated first, and the variant override is applied to the first matching condition.
322
+ sorted_flag_conditions = flag_conditions.each_with_index.sort_by { |condition, idx| [condition[:variant].nil? ? 1 : -1, idx] }
323
+
324
+ sorted_flag_conditions.each do |condition, idx|
325
+ begin
326
+ if is_condition_match(flag, distinct_id, condition, properties)
327
+ variant_override = condition[:variant]
328
+ flag_multivariate = flag_filters[:multivariate] || {}
329
+ flag_variants = flag_multivariate[:variants] || []
330
+ if flag_variants.map{|variant| variant[:key]}.include?(condition[:variant])
331
+ variant = variant_override
332
+ else
333
+ variant = get_matching_variant(flag, distinct_id)
334
+ end
335
+ result = variant || true
336
+ break
337
+ end
338
+ rescue InconclusiveMatchError => e
339
+ is_inconclusive = true
340
+ end
341
+ end
342
+
343
+ if !result.nil?
344
+ return result
345
+ elsif is_inconclusive
346
+ raise InconclusiveMatchError.new("Can't determine if feature flag is enabled or not with given properties")
347
+ end
348
+
349
+ # We can only return False when all conditions are False
350
+ return false
351
+ end
352
+
353
+ def is_condition_match(flag, distinct_id, condition, properties)
354
+ rollout_percentage = condition[:rollout_percentage]
355
+
356
+ if !(condition[:properties] || []).empty?
357
+ if !condition[:properties].all? { |prop|
358
+ FeatureFlagsPoller.match_property(prop, properties)
359
+ }
360
+ return false
361
+ elsif rollout_percentage.nil?
362
+ return true
363
+ end
364
+ end
365
+
366
+ if !rollout_percentage.nil? and _hash(flag[:key], distinct_id) > (rollout_percentage.to_f/100)
367
+ return false
368
+ end
369
+
370
+ return true
371
+ end
372
+
373
+ # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
374
+ # Given the same distinct_id and key, it'll always return the same float. These floats are
375
+ # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
376
+ # we can do _hash(key, distinct_id) < 0.2
377
+ def _hash(key, distinct_id, salt="")
378
+ hash_key = Digest::SHA1.hexdigest "#{key}.#{distinct_id}#{salt}"
379
+ return (Integer(hash_key[0..14], 16).to_f / 0xfffffffffffffff)
380
+ end
381
+
382
+ def get_matching_variant(flag, distinct_id)
383
+ hash_value = _hash(flag[:key], distinct_id, salt="variant")
384
+ matching_variant = variant_lookup_table(flag).find { |variant|
385
+ hash_value >= variant[:value_min] and hash_value < variant[:value_max]
386
+ }
387
+ matching_variant.nil? ? nil : matching_variant[:key]
388
+ end
389
+
390
+ def variant_lookup_table(flag)
391
+ lookup_table = []
392
+ value_min = 0
393
+ flag_filters = flag[:filters] || {}
394
+ variants = flag_filters[:multivariate] || {}
395
+ multivariates = variants[:variants] || []
396
+ multivariates.each do |variant|
397
+ value_max = value_min + variant[:rollout_percentage].to_f / 100
398
+ lookup_table << {'value_min': value_min, 'value_max': value_max, 'key': variant[:key]}
399
+ value_min = value_max
400
+ end
401
+ return lookup_table
402
+ end
403
+
404
+ def _load_feature_flags()
405
+ res = _request_feature_flag_definitions
406
+
407
+ if !res.key?(:flags)
408
+ logger.error "Failed to load feature flags: #{res}"
409
+ else
410
+ @feature_flags = res[:flags] || []
411
+ @feature_flags_by_key = {}
412
+ @feature_flags.each do |flag|
413
+ if flag[:key] != nil
414
+ @feature_flags_by_key[flag[:key]] = flag
415
+ end
416
+ end
417
+ @group_type_mapping = res[:group_type_mapping] || {}
418
+
419
+ logger.debug "Loaded #{@feature_flags.length} feature flags"
420
+ if @loaded_flags_successfully_once.false?
421
+ @loaded_flags_successfully_once.make_true
422
+ end
423
+ end
424
+ end
425
+
426
+ def _request_feature_flag_definitions
427
+ uri = URI("#{@host}/api/feature_flag/local_evaluation?token=#{@project_api_key}")
428
+ req = Net::HTTP::Get.new(uri)
429
+ req['Authorization'] = "Bearer #{@personal_api_key}"
430
+
431
+ _request(uri, req)
432
+ end
433
+
434
+ def _request_feature_flag_evaluation(data={})
435
+ uri = URI("#{@host}/decide/?v=3")
436
+ req = Net::HTTP::Post.new(uri)
437
+ req['Content-Type'] = 'application/json'
438
+ data['token'] = @project_api_key
439
+ req.body = data.to_json
440
+
441
+ _request(uri, req)
442
+ end
443
+
444
+ def _request(uri, request_object)
445
+
446
+ request_object['User-Agent'] = "posthog-ruby#{PostHog::VERSION}"
447
+
448
+ begin
449
+ res_body = nil
450
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
451
+ res = http.request(request_object)
452
+ JSON.parse(res.body, {symbolize_names: true})
453
+ end
454
+ rescue Timeout::Error,
455
+ Errno::EINVAL,
456
+ Errno::ECONNRESET,
457
+ EOFError,
458
+ Net::HTTPBadResponse,
459
+ Net::HTTPHeaderSyntaxError,
460
+ Net::ProtocolError => e
461
+ logger.debug("Unable to complete request to #{uri}")
462
+ throw e
463
+ end
464
+ end
465
+ end
466
+ end
@@ -0,0 +1,169 @@
1
+ class PostHog
2
+ class FieldParser
3
+ class << self
4
+ include PostHog::Utils
5
+
6
+ # In addition to the common fields, capture accepts:
7
+ #
8
+ # - "event"
9
+ # - "properties"
10
+ # - "groups"
11
+ def parse_for_capture(fields)
12
+ common = parse_common_fields(fields)
13
+
14
+ event = fields[:event]
15
+ properties = fields[:properties] || {}
16
+ groups = fields[:groups]
17
+
18
+ check_presence!(event, 'event')
19
+ check_is_hash!(properties, 'properties')
20
+
21
+ if groups
22
+ check_is_hash!(groups, 'groups')
23
+ properties["$groups"] = groups
24
+ end
25
+
26
+ isoify_dates! properties
27
+
28
+ common.merge(
29
+ {
30
+ type: 'capture',
31
+ event: event.to_s,
32
+ properties: properties.merge(common[:properties] || {})
33
+ }
34
+ )
35
+ end
36
+
37
+ # In addition to the common fields, identify accepts:
38
+ #
39
+ # - "properties"
40
+ def parse_for_identify(fields)
41
+ common = parse_common_fields(fields)
42
+
43
+ properties = fields[:properties] || {}
44
+ check_is_hash!(properties, 'properties')
45
+
46
+ isoify_dates! properties
47
+
48
+ common.merge(
49
+ {
50
+ type: 'identify',
51
+ event: '$identify',
52
+ '$set': properties,
53
+ properties: properties.merge(common[:properties] || {})
54
+ }
55
+ )
56
+ end
57
+
58
+ def parse_for_group_identify(fields)
59
+ properties = fields[:properties] || {}
60
+ group_type = fields[:group_type]
61
+ group_key = fields[:group_key]
62
+
63
+ check_presence!(group_type, 'group type')
64
+ check_presence!(group_key, 'group_key')
65
+ check_is_hash!(properties, 'properties')
66
+
67
+ distinct_id = "$#{group_type}_#{group_key}"
68
+ fields[:distinct_id] = distinct_id
69
+ common = parse_common_fields(fields)
70
+
71
+ isoify_dates! properties
72
+
73
+ common.merge(
74
+ {
75
+ event: '$groupidentify',
76
+ properties: {
77
+ "$group_type": group_type,
78
+ "$group_key": group_key,
79
+ "$group_set": properties.merge(common[:properties] || {})
80
+ },
81
+ }
82
+ )
83
+ end
84
+
85
+ # In addition to the common fields, alias accepts:
86
+ #
87
+ # - "alias"
88
+ def parse_for_alias(fields)
89
+ common = parse_common_fields(fields)
90
+
91
+ distinct_id = common[:distinct_id] # must both be set and move to properties
92
+
93
+ alias_field = fields[:alias]
94
+ check_presence! alias_field, 'alias'
95
+
96
+ common.merge(
97
+ {
98
+ type: 'alias',
99
+ event: '$create_alias',
100
+ distinct_id: distinct_id,
101
+ properties:
102
+ { distinct_id: distinct_id, alias: alias_field }.merge(
103
+ common[:properties] || {}
104
+ )
105
+ }
106
+ )
107
+ end
108
+
109
+ private
110
+
111
+ # Common fields are:
112
+ #
113
+ # - "timestamp"
114
+ # - "distinct_id"
115
+ # - "message_id"
116
+ # - "send_feature_flags"
117
+ def parse_common_fields(fields)
118
+ timestamp = fields[:timestamp] || Time.new
119
+ distinct_id = fields[:distinct_id]
120
+ message_id = fields[:message_id].to_s if fields[:message_id]
121
+ send_feature_flags = fields[:send_feature_flags]
122
+
123
+ check_timestamp! timestamp
124
+ check_presence! distinct_id, 'distinct_id'
125
+
126
+ parsed = {
127
+ timestamp: datetime_in_iso8601(timestamp),
128
+ library: 'posthog-ruby',
129
+ library_version: PostHog::VERSION.to_s,
130
+ messageId: message_id,
131
+ distinct_id: distinct_id,
132
+ properties: {
133
+ '$lib' => 'posthog-ruby',
134
+ '$lib_version' => PostHog::VERSION.to_s
135
+ }
136
+ }
137
+
138
+ if send_feature_flags
139
+ feature_variants = fields[:feature_variants]
140
+ feature_variants.each do |key, value|
141
+ parsed[:properties]["$feature/#{key}"] = value
142
+ end
143
+ parsed[:properties]["$active_feature_flags"] = feature_variants.keys
144
+ end
145
+ parsed
146
+ end
147
+
148
+ def check_timestamp!(timestamp)
149
+ unless timestamp.is_a? Time
150
+ raise ArgumentError, 'Timestamp must be a Time'
151
+ end
152
+ end
153
+
154
+ # private: Ensures that a string is non-empty
155
+ #
156
+ # obj - String|Number that must be non-blank
157
+ # name - Name of the validated value
158
+ def check_presence!(obj, name)
159
+ if obj.nil? || (obj.is_a?(String) && obj.empty?)
160
+ raise ArgumentError, "#{name} must be given"
161
+ end
162
+ end
163
+
164
+ def check_is_hash!(obj, name)
165
+ raise ArgumentError, "#{name} must be a Hash" unless obj.is_a? Hash
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,68 @@
1
+ require 'logger'
2
+
3
+ class PostHog
4
+ # Wraps an existing logger and adds a prefix to all messages
5
+ class PrefixedLogger
6
+ def initialize(logger, prefix)
7
+ @logger = logger
8
+ @prefix = prefix
9
+ end
10
+
11
+ def debug(msg)
12
+ @logger.debug("#{@prefix} #{msg}")
13
+ end
14
+
15
+ def info(msg)
16
+ @logger.info("#{@prefix} #{msg}")
17
+ end
18
+
19
+ def warn(msg)
20
+ @logger.warn("#{@prefix} #{msg}")
21
+ end
22
+
23
+ def error(msg)
24
+ @logger.error("#{@prefix} #{msg}")
25
+ end
26
+
27
+ def level=(severity)
28
+ @logger.level = severity
29
+ end
30
+
31
+ def level
32
+ @logger.level
33
+ end
34
+ end
35
+
36
+ module Logging
37
+ class << self
38
+ def logger
39
+ return @logger if @logger
40
+
41
+ base_logger =
42
+ if defined?(Rails)
43
+ Rails.logger
44
+ else
45
+ logger = Logger.new STDOUT
46
+ logger.progname = 'PostHog'
47
+ logger.level = Logger::WARN
48
+ logger
49
+ end
50
+ @logger = PrefixedLogger.new(base_logger, '[posthog-ruby]')
51
+ end
52
+
53
+ attr_writer :logger
54
+ end
55
+
56
+ def self.included(base)
57
+ class << base
58
+ def logger
59
+ Logging.logger
60
+ end
61
+ end
62
+ end
63
+
64
+ def logger
65
+ Logging.logger
66
+ end
67
+ end
68
+ end