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.
@@ -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