posthog-ruby 3.28.4 → 3.29.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 +4 -4
- data/lib/posthog/client.rb +61 -13
- data/lib/posthog/feature_flag_error.rb +2 -0
- data/lib/posthog/feature_flag_evaluations.rb +20 -2
- data/lib/posthog/feature_flags.rb +546 -148
- data/lib/posthog/flag_evaluation_errors.rb +35 -0
- data/lib/posthog/internal/context.rb +12 -6
- data/lib/posthog/internal/span_context.rb +34 -0
- data/lib/posthog/mcp/instrumentation.rb +3 -1
- data/lib/posthog/mcp/request_scope.rb +26 -15
- data/lib/posthog/otel/context_span_processor.rb +21 -0
- data/lib/posthog/unresolved_flag_reason.rb +20 -0
- data/lib/posthog/utils.rb +5 -6
- data/lib/posthog/version.rb +1 -1
- data/lib/posthog.rb +1 -0
- metadata +5 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'posthog/unresolved_flag_reason'
|
|
4
|
+
|
|
5
|
+
module PostHog
|
|
6
|
+
# InconclusiveMatchError is raised when local evaluation cannot determine a flag's
|
|
7
|
+
# value. {#reason} says why, as reported by {FeatureFlagEvaluations#unresolved_flags}.
|
|
8
|
+
class InconclusiveMatchError < StandardError
|
|
9
|
+
# @return [String] One of the {UnresolvedFlagReason} values.
|
|
10
|
+
attr_reader :reason
|
|
11
|
+
|
|
12
|
+
# @param message [String, nil]
|
|
13
|
+
# @param reason [String] One of the {UnresolvedFlagReason} values.
|
|
14
|
+
def initialize(message = nil, reason: UnresolvedFlagReason::UNSUPPORTED_DEFINITION)
|
|
15
|
+
super(message)
|
|
16
|
+
@reason = reason
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# RequiresServerEvaluation is raised when feature flag evaluation requires
|
|
21
|
+
# server-side data that is not available locally (e.g., static cohorts,
|
|
22
|
+
# experience continuity). This error should propagate immediately to trigger
|
|
23
|
+
# API fallback, unlike InconclusiveMatchError which allows trying other conditions.
|
|
24
|
+
class RequiresServerEvaluation < StandardError
|
|
25
|
+
# @return [String] One of the {UnresolvedFlagReason} values.
|
|
26
|
+
attr_reader :reason
|
|
27
|
+
|
|
28
|
+
# @param message [String, nil]
|
|
29
|
+
# @param reason [String] One of the {UnresolvedFlagReason} values.
|
|
30
|
+
def initialize(message = nil, reason: UnresolvedFlagReason::UNSUPPORTED_DEFINITION)
|
|
31
|
+
super(message)
|
|
32
|
+
@reason = reason
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'posthog/internal/span_context'
|
|
4
|
+
|
|
3
5
|
module PostHog
|
|
4
6
|
module Internal
|
|
5
7
|
# Internal request/fiber-local context applied to capture calls.
|
|
@@ -12,10 +14,12 @@ module PostHog
|
|
|
12
14
|
# server-side SDK globally stateful per user.
|
|
13
15
|
class Context
|
|
14
16
|
STORAGE_KEY = :posthog_context
|
|
17
|
+
UNBOUND_OWNER = Object.new.freeze
|
|
15
18
|
|
|
16
|
-
attr_reader :distinct_id, :session_id, :properties
|
|
19
|
+
attr_reader :distinct_id, :session_id, :properties, :owner
|
|
17
20
|
|
|
18
|
-
def initialize(distinct_id: nil, session_id: nil, properties: {})
|
|
21
|
+
def initialize(distinct_id: nil, session_id: nil, properties: {}, owner: UNBOUND_OWNER)
|
|
22
|
+
@owner = owner
|
|
19
23
|
@distinct_id = distinct_id
|
|
20
24
|
@session_id = session_id
|
|
21
25
|
@properties = properties ? properties.dup : {}
|
|
@@ -38,17 +42,18 @@ module PostHog
|
|
|
38
42
|
end
|
|
39
43
|
end
|
|
40
44
|
|
|
41
|
-
def self.with_context(data = nil, fresh: false, **kwargs)
|
|
45
|
+
def self.with_context(data = nil, fresh: false, owner: UNBOUND_OWNER, enrich_current_span: true, **kwargs)
|
|
42
46
|
previous_context = current
|
|
43
47
|
raise ArgumentError, 'with_context requires a block' unless block_given?
|
|
44
48
|
|
|
45
|
-
self.current = resolve(merge_data_and_kwargs(data, kwargs), previous_context, fresh: fresh)
|
|
49
|
+
self.current = resolve(merge_data_and_kwargs(data, kwargs), previous_context, fresh: fresh, owner: owner)
|
|
50
|
+
SpanContext.apply_current if enrich_current_span
|
|
46
51
|
yield
|
|
47
52
|
ensure
|
|
48
53
|
self.current = previous_context
|
|
49
54
|
end
|
|
50
55
|
|
|
51
|
-
def self.resolve(data, parent, fresh: false)
|
|
56
|
+
def self.resolve(data, parent, fresh: false, owner: UNBOUND_OWNER)
|
|
52
57
|
data = normalize_data(data)
|
|
53
58
|
|
|
54
59
|
parent_properties = fresh || parent.nil? ? {} : parent.properties
|
|
@@ -61,7 +66,8 @@ module PostHog
|
|
|
61
66
|
new(
|
|
62
67
|
distinct_id: data[:distinct_id] || (fresh || parent.nil? ? nil : parent.distinct_id),
|
|
63
68
|
session_id: data[:session_id] || (fresh || parent.nil? ? nil : parent.session_id),
|
|
64
|
-
properties: properties
|
|
69
|
+
properties: properties,
|
|
70
|
+
owner: owner.equal?(UNBOUND_OWNER) && parent ? parent.owner : owner
|
|
65
71
|
)
|
|
66
72
|
end
|
|
67
73
|
private_class_method :resolve
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostHog
|
|
4
|
+
module Internal
|
|
5
|
+
class SpanContext
|
|
6
|
+
def self.apply(span)
|
|
7
|
+
context = Context.current
|
|
8
|
+
return unless context && span&.recording? && span.respond_to?(:attributes)
|
|
9
|
+
|
|
10
|
+
return unless context.owner.equal?(Context::UNBOUND_OWNER) || context.owner&.enabled?
|
|
11
|
+
|
|
12
|
+
attributes = span.attributes || {}
|
|
13
|
+
{
|
|
14
|
+
'posthogDistinctId' => context.distinct_id,
|
|
15
|
+
'sessionId' => context.session_id
|
|
16
|
+
}.each do |key, value|
|
|
17
|
+
next if value.nil? || value == '' || attributes.key?(key)
|
|
18
|
+
|
|
19
|
+
span.set_attribute(key, value.is_a?(String) ? value.dup.freeze : value)
|
|
20
|
+
end
|
|
21
|
+
rescue StandardError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.apply_current
|
|
26
|
+
return unless defined?(::OpenTelemetry::Trace)
|
|
27
|
+
|
|
28
|
+
apply(::OpenTelemetry::Trace.current_span)
|
|
29
|
+
rescue StandardError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -588,7 +588,9 @@ module PostHog
|
|
|
588
588
|
'session_id' => (@session.respond_to?(:session_id) ? @session.session_id : nil) || header_session_id,
|
|
589
589
|
'request_id' => @request_id,
|
|
590
590
|
'protocol_version' => protocol_version,
|
|
591
|
-
|
|
591
|
+
# A deep copy, because session and protocol resolution read @headers after
|
|
592
|
+
# callbacks run, and the values are the Rack env's own Strings.
|
|
593
|
+
'headers' => @headers.transform_values(&:dup),
|
|
592
594
|
'session' => @session
|
|
593
595
|
}
|
|
594
596
|
end
|
|
@@ -17,14 +17,6 @@ module PostHog
|
|
|
17
17
|
KEY = :posthog_mcp_request_scope
|
|
18
18
|
FIBER_STORAGE = Fiber.respond_to?(:[]) && Fiber.respond_to?(:[]=)
|
|
19
19
|
|
|
20
|
-
# Rack `env` keys for the headers the integration reads, by header name.
|
|
21
|
-
HEADER_ENV_KEYS = {
|
|
22
|
-
'user-agent' => 'HTTP_USER_AGENT',
|
|
23
|
-
'x-anthropic-client' => 'HTTP_X_ANTHROPIC_CLIENT',
|
|
24
|
-
'mcp-session-id' => 'HTTP_MCP_SESSION_ID',
|
|
25
|
-
'mcp-protocol-version' => 'HTTP_MCP_PROTOCOL_VERSION'
|
|
26
|
-
}.freeze
|
|
27
|
-
|
|
28
20
|
module_function
|
|
29
21
|
|
|
30
22
|
# @return [Hash, nil] `{headers:, transport:, mint:, session_id:}` for the in-flight HTTP request,
|
|
@@ -46,20 +38,39 @@ module PostHog
|
|
|
46
38
|
end
|
|
47
39
|
end
|
|
48
40
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
#
|
|
41
|
+
# Rack `env` keys for the two request headers Rack stores without `HTTP_`.
|
|
42
|
+
UNPREFIXED_HEADER_ENV_KEYS = { 'CONTENT_TYPE' => 'content-type', 'CONTENT_LENGTH' => 'content-length' }.freeze
|
|
43
|
+
# The request line's protocol, which some servers (WEBrick, Thin) put in `env`.
|
|
44
|
+
PROTOCOL_ENV_KEY = 'HTTP_VERSION'
|
|
45
|
+
|
|
46
|
+
# Every request header, so `identify`, `intent_fallback` and `event_properties`
|
|
47
|
+
# can read the ones they need (an `authorization` bearer token, an API key)
|
|
48
|
+
# from `extra['headers']`. The SDK doesn't capture this bag: {Identity} keeps
|
|
49
|
+
# only scalars from `extra`. {TransportIdentity} still stamps `user-agent` and
|
|
50
|
+
# `x-anthropic-client`, as before. A callback that returns a raw header value in
|
|
51
|
+
# identity properties or `event_properties` sends it, unredacted.
|
|
52
|
+
#
|
|
52
53
|
# @param env [Hash] a Rack `env`
|
|
53
|
-
# @return [Hash{String => String}]
|
|
54
|
+
# @return [Hash{String => String}] lowercase header names
|
|
54
55
|
def headers_from_env(env)
|
|
55
56
|
return {} unless env.is_a?(Hash)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
acc[name] = value if value.is_a?(String) && !value.empty?
|
|
58
|
+
env.each_with_object({}) do |(key, value), acc|
|
|
59
|
+
name = header_name(key)
|
|
60
|
+
acc[name] = value if name && value.is_a?(String) && !value.empty?
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
|
|
64
|
+
def header_name(env_key)
|
|
65
|
+
return UNPREFIXED_HEADER_ENV_KEYS[env_key] unless env_key.is_a?(String) && env_key.start_with?('HTTP_')
|
|
66
|
+
return nil if env_key == PROTOCOL_ENV_KEY
|
|
67
|
+
|
|
68
|
+
env_key.delete_prefix('HTTP_').downcase.tr('_', '-')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param headers [Hash{String => String}] lowercase header names
|
|
72
|
+
# @param transport [Symbol] `:http` for the Streamable HTTP transport, `:other`
|
|
73
|
+
# for a transport that publishes no headers (stdio, a custom dispatcher)
|
|
63
74
|
def with(headers:, transport: :http)
|
|
64
75
|
previous = current
|
|
65
76
|
self.current = { headers: headers, transport: transport, mint: nil, session_id: nil }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'posthog/internal/context'
|
|
4
|
+
|
|
5
|
+
module PostHog
|
|
6
|
+
class ContextSpanProcessor
|
|
7
|
+
def on_start(span, _parent_context)
|
|
8
|
+
Internal::SpanContext.apply(span)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def on_finish(_span); end
|
|
12
|
+
|
|
13
|
+
def force_flush(**_options)
|
|
14
|
+
0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def shutdown(**_options)
|
|
18
|
+
0
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostHog
|
|
4
|
+
# Why local evaluation left a flag without a value, as reported by
|
|
5
|
+
# {FeatureFlagEvaluations#unresolved_flags}.
|
|
6
|
+
#
|
|
7
|
+
# Reason values:
|
|
8
|
+
# EXPERIENCE_CONTINUITY: The flag has experience continuity enabled, which local evaluation never resolves
|
|
9
|
+
# UNSUPPORTED_DEFINITION: The flag uses something local evaluation does not support, such as a static
|
|
10
|
+
# cohort, an unknown operator, a malformed value, or a missing or circular flag dependency
|
|
11
|
+
# UNRESOLVED_DEPENDENCY: A flag this flag depends on has a loaded definition but was unresolved
|
|
12
|
+
# MISSING_CONTEXT: The call did not pass a property or group property the flag needs, or passed it in a
|
|
13
|
+
# form local evaluation cannot use
|
|
14
|
+
module UnresolvedFlagReason
|
|
15
|
+
EXPERIENCE_CONTINUITY = 'experience_continuity'
|
|
16
|
+
UNSUPPORTED_DEFINITION = 'unsupported_definition'
|
|
17
|
+
UNRESOLVED_DEPENDENCY = 'unresolved_dependency'
|
|
18
|
+
MISSING_CONTEXT = 'missing_context'
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/posthog/utils.rb
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
class InconclusiveMatchError < StandardError
|
|
5
|
-
end
|
|
3
|
+
require 'posthog/flag_evaluation_errors'
|
|
6
4
|
|
|
5
|
+
module PostHog
|
|
7
6
|
# Utility helpers used internally by the SDK.
|
|
8
7
|
#
|
|
9
8
|
# @api private
|
|
@@ -92,7 +91,7 @@ module PostHog
|
|
|
92
91
|
seconds.abs / 3600, (seconds.abs % 3600) / 60)
|
|
93
92
|
end
|
|
94
93
|
|
|
95
|
-
def convert_to_datetime(value)
|
|
94
|
+
def convert_to_datetime(value, reason:)
|
|
96
95
|
if value.respond_to?(:strftime)
|
|
97
96
|
value
|
|
98
97
|
|
|
@@ -100,10 +99,10 @@ module PostHog
|
|
|
100
99
|
begin
|
|
101
100
|
DateTime.parse(value)
|
|
102
101
|
rescue ArgumentError
|
|
103
|
-
raise InconclusiveMatchError
|
|
102
|
+
raise InconclusiveMatchError.new("#{value} is not in a valid date format", reason: reason)
|
|
104
103
|
end
|
|
105
104
|
else
|
|
106
|
-
raise InconclusiveMatchError
|
|
105
|
+
raise InconclusiveMatchError.new('The date provided must be a string or date object', reason: reason)
|
|
107
106
|
end
|
|
108
107
|
end
|
|
109
108
|
|
data/lib/posthog/version.rb
CHANGED
data/lib/posthog.rb
CHANGED
|
@@ -11,6 +11,7 @@ require 'posthog/response'
|
|
|
11
11
|
require 'posthog/logging'
|
|
12
12
|
require 'posthog/exception_capture'
|
|
13
13
|
require 'posthog/feature_flag_error'
|
|
14
|
+
require 'posthog/unresolved_flag_reason'
|
|
14
15
|
require 'posthog/feature_flag_result'
|
|
15
16
|
require 'posthog/feature_flag_evaluations'
|
|
16
17
|
require 'posthog/flag_definition_cache'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: posthog-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.29.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ''
|
|
@@ -46,7 +46,9 @@ files:
|
|
|
46
46
|
- lib/posthog/feature_flags.rb
|
|
47
47
|
- lib/posthog/field_parser.rb
|
|
48
48
|
- lib/posthog/flag_definition_cache.rb
|
|
49
|
+
- lib/posthog/flag_evaluation_errors.rb
|
|
49
50
|
- lib/posthog/internal/context.rb
|
|
51
|
+
- lib/posthog/internal/span_context.rb
|
|
50
52
|
- lib/posthog/logging.rb
|
|
51
53
|
- lib/posthog/mcp.rb
|
|
52
54
|
- lib/posthog/mcp/README.md
|
|
@@ -75,10 +77,12 @@ files:
|
|
|
75
77
|
- lib/posthog/mcp/truncation.rb
|
|
76
78
|
- lib/posthog/message_batch.rb
|
|
77
79
|
- lib/posthog/noop_worker.rb
|
|
80
|
+
- lib/posthog/otel/context_span_processor.rb
|
|
78
81
|
- lib/posthog/response.rb
|
|
79
82
|
- lib/posthog/send_feature_flags_options.rb
|
|
80
83
|
- lib/posthog/send_worker.rb
|
|
81
84
|
- lib/posthog/transport.rb
|
|
85
|
+
- lib/posthog/unresolved_flag_reason.rb
|
|
82
86
|
- lib/posthog/utils.rb
|
|
83
87
|
- lib/posthog/version.rb
|
|
84
88
|
homepage: https://github.com/PostHog/posthog-ruby
|