///|
fn require(condition : Bool, message : String) -> Unit {
if !condition {
abort(message)
}
}
///|
/// One hook per scope exposes the actual ordering, including error unwinding.
fn observer(name : String, trace : Array[String]) -> @feature.Hook {
@feature.Hook::new(
name,
types=Some([@feature.FlagType::Boolean]),
before=(context, hints) => {
trace.push(name + ".before")
require(
context.data.get("scope") == None,
"hook scratch data starts empty per hook and evaluation",
)
context.data.set("scope", Json::string(name)).unwrap()
if name == "sdk" {
let delta = @feature.EvaluationContext::empty()
.with_attribute("cohort", Json::string("beta"))
.unwrap()
.with_attribute("traceId", hints.get("traceId").unwrap())
.unwrap()
Ok(Some(delta))
} else {
require(
context.evaluation_context.get("cohort") == Some(Json::string("beta")),
"earlier before-hook context must flow forward",
)
Ok(None)
}
},
after=(context, details, _) => {
trace.push(name + ".after")
require(
context.data.get("scope") == Some(Json::string(name)),
"after stage retains only its own hook data",
)
require(!details.is_error(), "after sees successful details")
require(
context.evaluation_context.get("traceId") ==
Some(Json::string("trace-42")),
"after sees final context",
)
Ok(())
},
error=(context, error, _) => {
trace.push(name + ".error:" + error.code.name())
require(
context.data.get("scope") == Some(Json::string(name)),
"error stage retains per-hook data",
)
if name == "invocation" {
Err(@feature.SdkError::new(General, "error observer sink unavailable"))
} else {
Ok(())
}
},
on_finally=(context, details, _) => {
trace.push(name + ".on_finally:" + details.reason)
require(
context.data.get("scope") == Some(Json::string(name)),
"completion stage retains per-hook data",
)
if name == "invocation" {
Err(
@feature.SdkError::new(
General,
"completion observer sink unavailable",
),
)
} else {
Ok(())
}
},
)
}
///|
fn main {
let sdk = @feature.Sdk::new()
sdk
.set_context(
@feature.EvaluationContext::from_attributes({
"region": Json::string("global"),
}).unwrap(),
)
.unwrap()
let client = sdk.client(domain="checkout", name="web-checkout")
client.set_context(
@feature.EvaluationContext::from_attributes({
"service": Json::string("checkout"),
})
.unwrap()
.with_targeting_key("customer-17"),
)
let trace : Array[String] = []
let lifecycle : Array[String] = []
let tracked : Array[@feature.TrackingEvent] = []
let tracking_contexts : Array[@feature.EvaluationContext] = []
let listener = sdk
.events()
.on(
event => {
let line = event.event_type.name() +
" provider=" +
event.provider_name +
" domain=" +
event.domain.unwrap()
lifecycle.push(line)
println("lifecycle: " + line)
Ok(())
},
domain=Some("checkout"),
)
let provider = @feature.Provider::new(
"observed-adapter",
(key, _, default, context) => {
if key == "express-checkout" {
@feature.Resolution::success(
@feature.FlagValue::Boolean(
context.get("cohort") == Some(Json::string("beta")),
),
reason="TARGETING_MATCH",
variant=Some("beta"),
)
} else {
@feature.Resolution::failure(
default,
@feature.SdkError::new(FlagNotFound, "missing flag: " + key),
)
}
},
track=Some((event, context) => {
if event.key == "analytics-unavailable" {
Err(@feature.SdkError::new(General, "analytics transport unavailable"))
} else {
tracked.push(event.copy())
tracking_contexts.push(context.copy())
println("tracking payload: " + event.to_json().stringify())
println("tracking context: " + context.to_json().stringify())
Ok(())
}
}),
)
sdk.add_hook(observer("sdk", trace))
client.add_hook(observer("client", trace))
provider.add_hook(observer("provider", trace))
sdk.set_provider(provider, domain="checkout").unwrap()
let options = @feature.EvaluationOptions::new(
hooks=[observer("invocation", trace)],
hints=@feature.EvaluationContext::from_attributes({
"traceId": Json::string("trace-42"),
}).unwrap(),
)
let success = client.boolean_details("express-checkout", false, options~)
require(
success.value && !success.is_error(),
"observer failures must not replace successful value",
)
require(
trace ==
[
"sdk.before", "client.before", "invocation.before", "provider.before", "provider.after",
"invocation.after", "client.after", "sdk.after", "provider.on_finally:TARGETING_MATCH",
"invocation.on_finally:TARGETING_MATCH", "client.on_finally:TARGETING_MATCH",
"sdk.on_finally:TARGETING_MATCH",
],
"success hook order",
)
println("success hooks: " + trace.join(" > "))
println("success details: " + success.details.to_json().stringify())
let success_diagnostics = sdk.take_diagnostics()
require(success_diagnostics.length() == 1, "completion observer diagnostic")
println(
"observer diagnostic: " + success_diagnostics[0].to_json().stringify(),
)
trace.clear()
let missing = client.boolean_details("missing", false, options~)
require(
!missing.value && missing.details.error is Some({ code: FlagNotFound, .. }),
"original evaluation error must survive observer errors",
)
require(
trace ==
[
"sdk.before", "client.before", "invocation.before", "provider.before", "provider.error:FLAG_NOT_FOUND",
"invocation.error:FLAG_NOT_FOUND", "client.error:FLAG_NOT_FOUND", "sdk.error:FLAG_NOT_FOUND",
"provider.on_finally:ERROR", "invocation.on_finally:ERROR", "client.on_finally:ERROR",
"sdk.on_finally:ERROR",
],
"error hook order and continued unwinding",
)
println("error hooks: " + trace.join(" > "))
println("error details: " + missing.details.to_json().stringify())
let error_diagnostics = sdk.take_diagnostics()
require(
error_diagnostics.length() == 2,
"both broken observers must be diagnosed",
)
for diagnostic in error_diagnostics {
println("observer diagnostic: " + diagnostic.to_json().stringify())
}
require(sdk.take_diagnostics().length() == 0, "diagnostics drain once")
require(
client.context().get("cohort") == None,
"hook context must not persist in client",
)
trace.clear()
let details = @feature.EvaluationContext::from_attributes({
"orderId": Json::string("order-42"),
"items": Json::array([Json::string("book"), Json::string("pen")]),
}).unwrap()
let event = @feature.TrackingEvent::new(
"purchase",
value=Some(79.5),
details~,
).unwrap()
client
.track_checked(
event,
transaction_context=@feature.EvaluationContext::from_attributes({
"requestId": Json::string("req-42"),
}).unwrap(),
context=@feature.EvaluationContext::from_attributes({
"region": Json::string("eu-west"),
}).unwrap(),
)
.unwrap()
require(
tracked.length() == 1 && tracked[0].value == Some(79.5),
"tracking callback records payload",
)
require(
tracking_contexts[0].targeting_key() == Some("customer-17"),
"tracking merges client targeting key",
)
require(
tracking_contexts[0].get("requestId") == Some(Json::string("req-42")),
"tracking transaction context",
)
require(
tracking_contexts[0].get("cohort") == None && trace.length() == 0,
"tracking is not evaluation and must not run hooks",
)
let failed_track = client.track_checked(
@feature.TrackingEvent::new("analytics-unavailable").unwrap(),
)
require(
failed_track is Err({ code: General, .. }),
"tracking error propagates explicitly",
)
println(
"tracking failure: " + failed_track.unwrap_err().to_json().stringify(),
)
provider.emit_configuration_changed(["express-checkout"])
require(
lifecycle.length() == 2,
"ready and configuration-change observability",
)
require(sdk.events().off(listener), "remove lifecycle listener")
require(
sdk.take_diagnostics().length() == 0,
"tracking errors are returned, not observer diagnostics",
)
// Standard Unit tracking never throws; failures remain observable via SDK diagnostics.
client.track(@feature.TrackingEvent::new("analytics-unavailable").unwrap())
let no_throw_diagnostics = sdk.take_diagnostics()
require(
no_throw_diagnostics.length() == 1 &&
no_throw_diagnostics[0].code == @feature.ErrorCode::General,
"Unit tracking preserves diagnostics",
)
println(
"no-throw tracking diagnostic: " +
no_throw_diagnostics[0].to_json().stringify(),
)
require(
trace.length() == 0 && tracked.length() == 1,
"failed tracking must not run evaluation hooks or record a purchase",
)
require(
sdk.take_diagnostics().length() == 0,
"tracking diagnostics drain once",
)
sdk.shutdown().unwrap()
}