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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Portions of this file are derived from getsentry/sentry-ruby by Software, Inc. dba Sentry
|
|
4
|
+
# Licensed under the MIT License
|
|
5
|
+
# - sentry-ruby/lib/sentry/interfaces/single_exception.rb
|
|
6
|
+
# - sentry-ruby/lib/sentry/interfaces/stacktrace_builder.rb
|
|
7
|
+
# - sentry-ruby/lib/sentry/backtrace.rb
|
|
8
|
+
# - sentry-ruby/lib/sentry/interfaces/stacktrace.rb
|
|
9
|
+
# - sentry-ruby/lib/sentry/linecache.rb
|
|
10
|
+
|
|
11
|
+
# 💖 open source (under MIT License)
|
|
12
|
+
|
|
13
|
+
module PostHog
|
|
14
|
+
module ExceptionCapture
|
|
15
|
+
RUBY_INPUT_FORMAT = /
|
|
16
|
+
^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>):
|
|
17
|
+
(\d+)
|
|
18
|
+
(?: :in\s('|`)(?:([\w:]+)\#)?([^']+)')?$
|
|
19
|
+
/x
|
|
20
|
+
|
|
21
|
+
def self.build_parsed_exception(value)
|
|
22
|
+
title, message, backtrace = coerce_exception_input(value)
|
|
23
|
+
return nil if title.nil?
|
|
24
|
+
|
|
25
|
+
build_single_exception_from_data(title, message, backtrace)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.build_single_exception_from_data(title, message, backtrace)
|
|
29
|
+
{
|
|
30
|
+
'type' => title,
|
|
31
|
+
'value' => message || '',
|
|
32
|
+
'mechanism' => {
|
|
33
|
+
'type' => 'generic',
|
|
34
|
+
'handled' => true
|
|
35
|
+
},
|
|
36
|
+
'stacktrace' => build_stacktrace(backtrace)
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.build_stacktrace(backtrace)
|
|
41
|
+
return nil unless backtrace && !backtrace.empty?
|
|
42
|
+
|
|
43
|
+
frames = backtrace.first(50).map do |line|
|
|
44
|
+
parse_backtrace_line(line)
|
|
45
|
+
end.compact.reverse
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
'type' => 'raw',
|
|
49
|
+
'frames' => frames
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.parse_backtrace_line(line)
|
|
54
|
+
match = line.match(RUBY_INPUT_FORMAT)
|
|
55
|
+
return nil unless match
|
|
56
|
+
|
|
57
|
+
file = match[1]
|
|
58
|
+
lineno = match[2].to_i
|
|
59
|
+
method_name = match[5]
|
|
60
|
+
|
|
61
|
+
frame = {
|
|
62
|
+
'filename' => File.basename(file),
|
|
63
|
+
'abs_path' => file,
|
|
64
|
+
'lineno' => lineno,
|
|
65
|
+
'function' => method_name,
|
|
66
|
+
'in_app' => !gem_path?(file),
|
|
67
|
+
'platform' => 'ruby'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
add_context_lines(frame, file, lineno) if File.exist?(file)
|
|
71
|
+
|
|
72
|
+
frame
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.gem_path?(path)
|
|
76
|
+
path.include?('/gems/') ||
|
|
77
|
+
path.include?('/ruby/') ||
|
|
78
|
+
path.include?('/.rbenv/') ||
|
|
79
|
+
path.include?('/.rvm/')
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.add_context_lines(frame, file_path, lineno, context_size = 5)
|
|
83
|
+
lines = File.readlines(file_path)
|
|
84
|
+
return if lines.empty?
|
|
85
|
+
|
|
86
|
+
return unless lineno.positive? && lineno <= lines.length
|
|
87
|
+
|
|
88
|
+
pre_context_start = [lineno - context_size, 1].max
|
|
89
|
+
post_context_end = [lineno + context_size, lines.length].min
|
|
90
|
+
|
|
91
|
+
frame['context_line'] = lines[lineno - 1].chomp
|
|
92
|
+
|
|
93
|
+
frame['pre_context'] = lines[(pre_context_start - 1)...(lineno - 1)].map(&:chomp) if pre_context_start < lineno
|
|
94
|
+
|
|
95
|
+
frame['post_context'] = lines[lineno...(post_context_end)].map(&:chomp) if post_context_end > lineno
|
|
96
|
+
rescue StandardError
|
|
97
|
+
# Silently ignore file read errors
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.coerce_exception_input(value)
|
|
101
|
+
if value.is_a?(String)
|
|
102
|
+
title = 'Error'
|
|
103
|
+
message = value
|
|
104
|
+
backtrace = nil
|
|
105
|
+
elsif value.respond_to?(:backtrace) && value.respond_to?(:message)
|
|
106
|
+
title = value.class.to_s
|
|
107
|
+
message = value.message || ''
|
|
108
|
+
backtrace = value.backtrace
|
|
109
|
+
else
|
|
110
|
+
return [nil, nil, nil]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
[title, message, backtrace]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Represents a feature flag returned by /flags v2
|
|
4
|
+
module PostHog
|
|
5
|
+
class FeatureFlag
|
|
6
|
+
attr_reader :key, :enabled, :variant, :reason, :metadata
|
|
7
|
+
|
|
8
|
+
def initialize(json)
|
|
9
|
+
json.transform_keys!(&:to_s)
|
|
10
|
+
@key = json['key']
|
|
11
|
+
@enabled = json['enabled']
|
|
12
|
+
@variant = json['variant']
|
|
13
|
+
@reason = json['reason'] ? EvaluationReason.new(json['reason']) : nil
|
|
14
|
+
@metadata = json['metadata'] ? FeatureFlagMetadata.new(json['metadata'].transform_keys(&:to_s)) : nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# TODO: Rename to `value` in future version
|
|
18
|
+
def get_value # rubocop:disable Naming/AccessorMethodName
|
|
19
|
+
@variant || @enabled
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def payload
|
|
23
|
+
@metadata&.payload
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.from_value_and_payload(key, value, payload)
|
|
27
|
+
new({
|
|
28
|
+
'key' => key,
|
|
29
|
+
'enabled' => value.is_a?(String) || value,
|
|
30
|
+
'variant' => value.is_a?(String) ? value : nil,
|
|
31
|
+
'reason' => nil,
|
|
32
|
+
'metadata' => {
|
|
33
|
+
'id' => nil,
|
|
34
|
+
'version' => nil,
|
|
35
|
+
'payload' => payload,
|
|
36
|
+
'description' => nil
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Represents the reason why a flag was enabled/disabled
|
|
43
|
+
class EvaluationReason
|
|
44
|
+
attr_reader :code, :description, :condition_index
|
|
45
|
+
|
|
46
|
+
def initialize(json)
|
|
47
|
+
json.transform_keys!(&:to_s)
|
|
48
|
+
@code = json['code']
|
|
49
|
+
@description = json['description']
|
|
50
|
+
@condition_index = json['condition_index'].to_i if json['condition_index']
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Represents metadata about a feature flag
|
|
55
|
+
class FeatureFlagMetadata
|
|
56
|
+
attr_reader :id, :version, :payload, :description
|
|
57
|
+
|
|
58
|
+
def initialize(json)
|
|
59
|
+
json.transform_keys!(&:to_s)
|
|
60
|
+
@id = json['id']
|
|
61
|
+
@version = json['version']
|
|
62
|
+
@payload = json['payload']
|
|
63
|
+
@description = json['description']
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostHog
|
|
4
|
+
# Error type constants for the $feature_flag_error property.
|
|
5
|
+
#
|
|
6
|
+
# These values are sent in analytics events to track flag evaluation failures.
|
|
7
|
+
# They should not be changed without considering impact on existing dashboards
|
|
8
|
+
# and queries that filter on these values.
|
|
9
|
+
#
|
|
10
|
+
# Error values:
|
|
11
|
+
# ERRORS_WHILE_COMPUTING: Server returned errorsWhileComputingFlags=true
|
|
12
|
+
# FLAG_MISSING: Requested flag not in API response
|
|
13
|
+
# QUOTA_LIMITED: Rate/quota limit exceeded
|
|
14
|
+
# TIMEOUT: Request timed out
|
|
15
|
+
# CONNECTION_ERROR: Network connectivity issue
|
|
16
|
+
# UNKNOWN_ERROR: Unexpected exceptions
|
|
17
|
+
#
|
|
18
|
+
# For API errors with status codes, use the api_error() method which returns
|
|
19
|
+
# a string like "api_error_500".
|
|
20
|
+
class FeatureFlagError
|
|
21
|
+
ERRORS_WHILE_COMPUTING = 'errors_while_computing_flags'
|
|
22
|
+
FLAG_MISSING = 'flag_missing'
|
|
23
|
+
QUOTA_LIMITED = 'quota_limited'
|
|
24
|
+
TIMEOUT = 'timeout'
|
|
25
|
+
CONNECTION_ERROR = 'connection_error'
|
|
26
|
+
UNKNOWN_ERROR = 'unknown_error'
|
|
27
|
+
|
|
28
|
+
# Generate API error string with status code.
|
|
29
|
+
#
|
|
30
|
+
# @param status [Integer, String] The HTTP status code
|
|
31
|
+
# @return [String] Error string in format "api_error_STATUS"
|
|
32
|
+
def self.api_error(status)
|
|
33
|
+
"api_error_#{status.to_s.downcase}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module PostHog
|
|
6
|
+
# Represents the result of a feature flag evaluation
|
|
7
|
+
# containing both the flag value and payload
|
|
8
|
+
class FeatureFlagResult
|
|
9
|
+
attr_reader :key, :variant, :payload
|
|
10
|
+
|
|
11
|
+
def initialize(key:, enabled:, variant: nil, payload: nil)
|
|
12
|
+
@key = key
|
|
13
|
+
@enabled = enabled
|
|
14
|
+
@variant = variant
|
|
15
|
+
@payload = payload
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns the effective value of the feature flag
|
|
19
|
+
# variant if present, otherwise enabled status
|
|
20
|
+
def value
|
|
21
|
+
@variant || @enabled
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns whether or not the feature flag evaluated as enabled
|
|
25
|
+
def enabled?
|
|
26
|
+
@enabled
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Factory method to create from flag value and payload
|
|
30
|
+
def self.from_value_and_payload(key, value, payload)
|
|
31
|
+
return nil if value.nil?
|
|
32
|
+
|
|
33
|
+
parsed_payload = parse_payload(payload)
|
|
34
|
+
|
|
35
|
+
if value.is_a?(String)
|
|
36
|
+
new(key: key, enabled: true, variant: value, payload: parsed_payload)
|
|
37
|
+
else
|
|
38
|
+
new(key: key, enabled: value, payload: parsed_payload)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.parse_payload(payload)
|
|
43
|
+
return nil if payload.nil?
|
|
44
|
+
return payload unless payload.is_a?(String)
|
|
45
|
+
return nil if payload.empty?
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
JSON.parse(payload)
|
|
49
|
+
rescue JSON::ParserError
|
|
50
|
+
payload
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private_class_method :parse_payload
|
|
55
|
+
end
|
|
56
|
+
end
|