Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Merging hooks work into main #267

Merged
merged 4 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: Run CI
on:
push:
branches: [ main ]
branches: [ main, 'feat/**' ]
paths-ignore:
- '**.md' # Do not need to run CI for markdown changes.
pull_request:
branches: [ main ]
branches: [ main, 'feat/**' ]
paths-ignore:
- '**.md'

Expand Down
7 changes: 7 additions & 0 deletions contract-tests/client_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'net/http'
require 'launchdarkly-server-sdk'
require './big_segment_store_fixture'
require './hook'
require 'http'

class ClientEntity
Expand Down Expand Up @@ -62,6 +63,12 @@ def initialize(log, config)
}
end

if config[:hooks]
opts[:hooks] = config[:hooks][:hooks].map do |hook|
Hook.new(hook[:name], hook[:callbackUri], hook[:data] || {}, hook[:errors] || {})
end
end

startWaitTimeMs = config[:startWaitTimeMs] || 5_000

@client = LaunchDarkly::LDClient.new(
Expand Down
74 changes: 74 additions & 0 deletions contract-tests/hook.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require 'ldclient-rb'

class Hook
include LaunchDarkly::Interfaces::Hooks::Hook

#
# @param name [String]
# @param callback_uri [String]
# @param data [Hash]
# @parm errors [Hash]
#
def initialize(name, callback_uri, data, errors)
@metadata = LaunchDarkly::Interfaces::Hooks::Metadata.new(name)
@callback_uri = callback_uri
@data = data
@errors = errors
@context_filter = LaunchDarkly::Impl::ContextFilter.new(false, [])
end

def metadata
@metadata
end

#
# @param evaluation_series_context [LaunchDarkly::Interfaces::Hooks::EvaluationSeriesContext]
# @param data [Hash]
#
def before_evaluation(evaluation_series_context, data)
raise @errors[:beforeEvaluation] if @errors.include? :beforeEvaluation

payload = {
evaluationSeriesContext: {
flagKey: evaluation_series_context.key,
context: @context_filter.filter(evaluation_series_context.context),
defaultValue: evaluation_series_context.default_value,
method: evaluation_series_context.method,
},
evaluationSeriesData: data,
stage: 'beforeEvaluation',
}
result = HTTP.post(@callback_uri, json: payload)

(data || {}).merge(@data[:beforeEvaluation] || {})
end


#
# @param evaluation_series_context [LaunchDarkly::Interfaces::Hooks::EvaluationSeriesContext]
# @param data [Hash]
# @param detail [LaunchDarkly::EvaluationDetail]
#
def after_evaluation(evaluation_series_context, data, detail)
raise @errors[:afterEvaluation] if @errors.include? :afterEvaluation

payload = {
evaluationSeriesContext: {
flagKey: evaluation_series_context.key,
context: @context_filter.filter(evaluation_series_context.context),
defaultValue: evaluation_series_context.default_value,
method: evaluation_series_context.method,
},
evaluationSeriesData: data,
evaluationDetail: {
value: detail.value,
variationIndex: detail.variation_index,
reason: detail.reason,
},
stage: 'afterEvaluation',
}
HTTP.post(@callback_uri, json: payload)

(data || {}).merge(@data[:afterEvaluation] || {})
end
end
1 change: 1 addition & 0 deletions contract-tests/service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
'polling-gzip',
'inline-context',
'anonymous-redaction',
'evaluation-hooks',
],
}.to_json
end
Expand Down
13 changes: 13 additions & 0 deletions lib/ldclient-rb/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Config
# @option opts [BigSegmentsConfig] :big_segments See {#big_segments}.
# @option opts [Hash] :application See {#application}
# @option opts [String] :payload_filter_key See {#payload_filter_key}
# @option hooks [Array<Interfaces::Hooks::Hook]
#
def initialize(opts = {})
@base_uri = (opts[:base_uri] || Config.default_base_uri).chomp("/")
Expand Down Expand Up @@ -75,6 +76,7 @@ def initialize(opts = {})
@big_segments = opts[:big_segments] || BigSegmentsConfig.new(store: nil)
@application = LaunchDarkly::Impl::Util.validate_application_info(opts[:application] || {}, @logger)
@payload_filter_key = opts[:payload_filter_key]
@hooks = (opts[:hooks] || []).keep_if { |hook| hook.is_a? Interfaces::Hooks::Hook }
@data_source_update_sink = nil
end

Expand Down Expand Up @@ -372,6 +374,17 @@ def diagnostic_opt_out?
#
attr_reader :socket_factory

#
# Initial set of hooks for the client.
#
# Hooks provide entrypoints which allow for observation of SDK functions.
#
# LaunchDarkly provides integration packages, and most applications will not
# need to implement their own hooks. Refer to the `launchdarkly-server-sdk-otel` gem
# for instrumentation.
#
attr_reader :hooks

#
# The default LaunchDarkly client configuration. This configuration sets
# reasonable defaults for most users.
Expand Down
34 changes: 34 additions & 0 deletions lib/ldclient-rb/impl/evaluation_with_hook_result.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module LaunchDarkly
module Impl
#
# Simple helper class for returning formatted data.
#
# The variation methods make use of the new hook support. Those methods all need to return an evaluation detail, and
# some other unstructured bit of data.
#
class EvaluationWithHookResult
#
# Return the evaluation detail that was generated as part of the evaluation.
#
# @return [LaunchDarkly::EvaluationDetail]
#
attr_reader :evaluation_detail

#
# All purpose container for additional return values from the wrapping method
#
# @return [any]
#
attr_reader :results

#
# @param evaluation_detail [LaunchDarkly::EvaluationDetail]
# @param results [any]
#
def initialize(evaluation_detail, results = nil)
@evaluation_detail = evaluation_detail
@results = results
end
end
end
end
85 changes: 85 additions & 0 deletions lib/ldclient-rb/interfaces.rb
Original file line number Diff line number Diff line change
Expand Up @@ -885,5 +885,90 @@ def build
end
end
end

module Hooks
#
# Mixin for extending SDK functionality via hooks.
#
# All provided hook implementations **MUST** include this mixin. Hooks without this mixin will be ignored.
#
# This mixin includes default implementations for all hook handlers. This allows LaunchDarkly to expand the list
# of hook handlers without breaking customer integrations.
#
module Hook
#
# Get metadata about the hook implementation.
#
# @return [Metadata]
#
def metadata
Metadata.new('UNDEFINED')
end

#
# The before method is called during the execution of a variation method before the flag value has been
# determined. The method is executed synchronously.
#
# @param evaluation_series_context [EvaluationSeriesContext] Contains information about the evaluation being
# performed. This is not mutable.
# @param data [Hash] A record associated with each stage of hook invocations. Each stage is called with the data
# of the previous stage for a series. The input record should not be modified.
# @return [Hash] Data to use when executing the next state of the hook in the evaluation series.
#
def before_evaluation(evaluation_series_context, data)
data
end

#
# The after method is called during the execution of the variation method after the flag value has been
# determined. The method is executed synchronously.
#
# @param evaluation_series_context [EvaluationSeriesContext] Contains read-only information about the evaluation
# being performed.
# @param data [Hash] A record associated with each stage of hook invocations. Each stage is called with the data
# of the previous stage for a series.
# @param detail [LaunchDarkly::EvaluationDetail] The result of the evaluation. This value should not be
# modified.
# @return [Hash] Data to use when executing the next state of the hook in the evaluation series.
#
def after_evaluation(evaluation_series_context, data, detail)
data
end
end

#
# Metadata data class used for annotating hook implementations.
#
class Metadata
attr_reader :name

def initialize(name)
@name = name
end
end

#
# Contextual information that will be provided to handlers during evaluation series.
#
class EvaluationSeriesContext
attr_reader :key
attr_reader :context
attr_reader :default_value
attr_reader :method

#
# @param key [String]
# @param context [LaunchDarkly::LDContext]
# @param default_value [any]
# @param method [Symbol]
#
def initialize(key, context, default_value, method)
@key = key
@context = context
@default_value = default_value
@method = method
end
end
end
end
end
Loading