reddit_decider
¶
Prerequisite packages¶
baseplate>=2.0.0
reddit-edgecontext>=1.0.0
reddit-v2-events>=2.8.2
Prerequisite infrastructure¶
Zookeeper live-data sidecar:
Define a live-data sidecar in the manifest file to fetch the experiment configuration, see example setup here (make sure that your service is authorized to fetch the appropriate secret from Vault).
Event publisher sidecar:
Set up your service to be able to publish v2 exposure events via an event publisher sidecar, see example setup here.
Prerequisite configuration¶
Setup reddit-experiments
in your application’s configuration file:
[app:main]
...
# optional: a path to the file where experiment configuration is written
# default: /var/local/experiments.json
# note: production systems load the experiments.json file under nested `live-data/` dir
experiments.path = /var/local/live-data/experiments.json
# optional: how long to wait for the experiments file to exist before failing
# default:
# >= v1.7.0 wait 30 seconds
# < v1.7.0 do not wait, fail immediately if not available
experiments.timeout = 60 seconds
# optional: the base amount of time for exponential backoff while waiting
# for the file to be available.
# default: 0.01
# see https://github.com/reddit/baseplate.py/blob/114248987ce0e8a0ddd102c80b00ef43f4dbf14e/baseplate/lib/file_watcher.py#L56C1-L56C35
experiments.backoff = 1 second
...
Integrate reddit-experiments
into Baseplate service¶
Upgrade or integrate reddit-experiments package:
# import latest reddit-experiments package in service requirements.txt
reddit-experiments>=1.7.0
Initialize decider
instance on Baseplate context¶
In your service’s initialization process, add a decider
instance to baseplate’s context:
# application code
from event_utils.v2_event_utils import ExperimentLogger
from reddit_decider import decider_client_from_config
from reddit_decider import DeciderClient
# optional
from some_file import my_field_extractor
def make_wsgi_app(app_config):
baseplate = Baseplate(app_config)
decider_factory = decider_client_from_config(app_config=app_config,
event_logger=ExperimentLogger(),
prefix="experiments.",
request_field_extractor=my_field_extractor) # this is optional, can be `None` if edge_context contains all the fields you need
baseplate.add_to_context("decider", decider_factory)
# Or use `DeciderClient` with `configure_context()`,
# which internally calls `decider_client_from_config()`
baseplate.configure_context({
"decider": DeciderClient(
prefix="experiments.",
event_logger=ExperimentLogger()),
request_field_extractor=my_field_extractor # optional
})
Make sure EdgeContext
is accessible on request
object like so:
request.edge_context
If you don’t have access to edge_context
in your service/request, you can access the SDK’s internal decider instance for a lower level API,
allowing you to pass in targeting context fields as a dict
param,
e.g. “user_is_employee”, “country_code”, or other targeting fields (instead of them being auto-derived from edge_context
).
See full API in readme (reddit internal).
The internal decider instance can be accessed from the SDK’s top-level decider instance via:
internal_decider = request.decider.internal_decider() # requires `reddit-experiments >= 1.4.1`
internal_decider.choose("experiment_name", {
"user_id": "t2_abc",
"user_is_employee": True,
"other_info": { "arbitrary_field": "some_val" }
}
)
[Optional] Define request field extractor function (example)
# Baseplate calls `make_object_for_context()` and creates a `DeciderContext`
# which fetches the following fields from EdgeContext automatically:
# - user_id
# - device_id
# - logged_in
# - cookie_created_timestamp
# - oauth_client_id
# - country_code
# - locale
# - origin_service
# - is_employee
# - loid_created_ms (>=1.3.11)
# Customized fields can be defined below to be extracted from a baseplate request
# and will override above edge_context fields.
# These fields may be used for targeting.
def my_field_extractor(request):
# an example of customized baseplate request field extractor:
return {"foo": request.headers.get("Foo"), "bar": "something"}
Basic Usage¶
Use the attached Decider
instance in request to call
decider.get_variant()
(automatically sends an expose event):
def my_method(request):
if request.decider.get_variant("foo") == "bar":
...
or optionally, if manual exposure is necessary, use:
def my_method(request):
variant = request.decider.get_variant_without_expose(experiment_name='experiment_name')
...
request.decider.expose(experiment_name='experiment_name', variant_name=variant)
This is an example of using a dynamic configuration:
def my_method(request):
if request.decider.get_bool("foo") == True:
...
Decider API¶
-
class
reddit_decider.
Decider
(decider_context, internal, server_span, context_name, event_logger=None)[source]¶ Access to experiments with automatic refresh when changed.
This decider client allows access to the experiments cached on disk by the experiment configuration fetcher daemon. It will automatically reload the cache when changed.
-
get_variant
(experiment_name, **exposure_kwargs)[source]¶ Return a bucketing variant, if any, with auto-exposure.
Since calling
get_variant()
will fire an exposure event, it is best to call it when you are sure the user will be exposed to the experiment.If you absolutely must check the status of an experiment before the user will be exposed to the experiment, use
get_variant_without_expose()
to disable exposure events and callexpose()
manually later.- Parameters
- Return type
- Returns
Variant name if a variant is assigned,
None
otherwise.
-
get_variant_without_expose
(experiment_name)[source]¶ Return a bucketing variant, if any, without emitting exposure event.
The
expose()
function is available to be manually called afterward.However, experiments in Holdout Groups will still send an exposure for the holdout parent experiment, since it is not possible to manually expose the holdout later (because it’s impossible to know if a returned
None
or"control_1"
string came from the holdout group or its child experiment once this function exits).
-
expose
(experiment_name, variant_name, **exposure_kwargs)[source]¶ Log an event to indicate that a user has been exposed to an experimental treatment.
Meant to be used after calling
get_variant_without_expose()
sinceget_variant()
emits exposure event automatically.- Parameters
experiment_name (
str
) – Name of the experiment that was exposed.variant_name (
str
) – Name of the variant that was exposed.exposure_kwargs (
Optional
[Dict
[str
,Any
]]) – Additional arguments that will be passed toevents_logger
(keys must be part of v2 event schema, use dicts for nested fields) underinputs
and askwargs
- Return type
-
get_variant_for_identifier
(experiment_name, identifier, identifier_type, **exposure_kwargs)[source]¶ Return a bucketing variant, if any, with auto-exposure for a given
identifier
.- Note: If the experiment’s
bucket_val
(e.g. “user_id”, “device_id”, “canonical_url”, “subreddit_id”, “ad_account_id”, “business_id”) does not match the
identifier_type
param, theidentifier
will be ignored and not used to bucket ({identifier_type: identifier}
is added to internalDeciderContext
instance, but doesn’t act like a bucketing override).If the
bucket_val
field exists on theDeciderContext
instance, that field will be used to bucket, since it corresponds to the experiment’s config.
Since calling
get_variant_for_identifier()
will fire an exposure event, it is best to call it when you are sure the user will be exposed to the experiment.- Parameters
experiment_name (
str
) – Name of the experiment you want a variant for.identifier (
str
) – an arbitary string used to bucket the experiment by being set onDeciderContext
’sidentifier_type
field.identifier_type (
Literal
[‘user_id’, ‘device_id’, ‘canonical_url’, ‘subreddit_id’, ‘ad_account_id’, ‘business_id’]) – Sets{identifier_type: identifier}
onDeciderContext
. The experiment’sbucket_val
will be looked up inDeciderContext
and be used to bucket. If the experiment’sbucket_val
field does not matchidentifier_type
param,identifier
will be ignored, and the field correspondingbucket_val
will be looked up fromDeciderContext
for bucketing.exposure_kwargs (
Optional
[Dict
[str
,Any
]]) – Additional arguments that will be passed toevents_logger
(keys must be part of v2 event schema, use dicts for nested fields) underinputs
and askwargs
- Return type
- Returns
Variant name if a variant is assigned, None otherwise.
- Note: If the experiment’s
-
get_variant_for_identifier_without_expose
(experiment_name, identifier, identifier_type)[source]¶ Return a bucketing variant, if any, without emitting exposure event for a given
identifier
.- Note: If the experiment’s
bucket_val
(e.g. “user_id”, “device_id”, “canonical_url”, “subreddit_id”, “ad_account_id”, “business_id”) does not match the
identifier_type
param, theidentifier
will be ignored and not used to bucket ({identifier_type: identifier}
is added to internalDeciderContext
instance, but doesn’t act like a bucketing override).If the
bucket_val
field exists on theDeciderContext
instance, that field will be used to bucket, since it corresponds to the experiment’s config.
The
expose()
function is available to be manually called afterward to emit exposure event.However, experiments in Holdout Groups will still send an exposure for the holdout parent experiment, since it is not possible to manually expose the holdout later (because it’s impossible to know if a returned
None
or"control_1"
string came from the holdout group or its child experiment once this function exits).- Parameters
experiment_name (
str
) – Name of the experiment you want a variant for.identifier (
str
) – an arbitary string used to bucket the experiment by being set onDeciderContext
’sidentifier_type
field.identifier_type (
Literal
[‘user_id’, ‘device_id’, ‘canonical_url’, ‘subreddit_id’, ‘ad_account_id’, ‘business_id’]) – Sets{identifier_type: identifier}
onDeciderContext
. The experiment’sbucket_val
will be looked up inDeciderContext
and be used to bucket. If the experiment’sbucket_val
field does not matchidentifier_type
param,identifier
will be ignored and the field correspondingbucket_val
will be looked up fromDeciderContext
for bucketing.
- Return type
- Returns
Variant name if a variant is assigned, None otherwise.
- Note: If the experiment’s
-
get_all_variants_without_expose
()[source]¶ Return a list of experiment dicts in this format:
[ { "id": 1, "name": "variant_1", "version": "1", "experimentName": "exp_1" } ]
If an experiment has a variant of
None
, it is not included in the returned list. All available experiments get bucketed. Exposure events are not emitted.The
expose()
function is available to be manually called afterward to emit exposure event.However, experiments in Holdout Groups will still send an exposure for the holdout parent experiment, since it is not possible to manually expose the holdout later (because it’s impossible to know if a returned
None
or"control_1"
string came from the holdout group or its child experiment once this function exits).
-
get_all_variants_for_identifier_without_expose
(identifier, identifier_type)[source]¶ Return a list of experiment dicts for experiments having
bucket_val
matchidentifier_type
, for a givenidentifier
, in this format:[ { "id": 1, "name": "variant_1", "version": "1", "experimentName": "exp_1" } ]
If an experiment has a variant of
None
, it is not included in the returned list. All available experiments get bucketed. Exposure events are not emitted.However, experiments in Holdout Groups will still send an exposure for the holdout parent experiment, since it is not possible to manually expose the holdout later (because it’s impossible to know if a returned
None
or"control_1"
string came from the holdout group or its child experiment once this function exits).- Parameters
identifier (
str
) – an arbitary string used to bucket the experiment by being set onDeciderContext
’sidentifier_type
field.identifier_type (
Literal
[‘user_id’, ‘device_id’, ‘canonical_url’, ‘subreddit_id’, ‘ad_account_id’, ‘business_id’]) – Sets{identifier_type: identifier}
on DeciderContext and buckets all experiment with matchingbucket_val
.
- Return type
- Returns
list of experiment dicts with non-
None
variants.
-
get_map
(feature_name, default=None)[source]¶ Fetch a Dynamic Configuration of map type.
- Parameters
- Return type
- Returns
the map value of the dyanimc config if it is active/exists,
default
parameter otherwise.
-
get_all_dynamic_configs
()[source]¶ Return a list of dynamic configuration dicts in this format:
[ { "name": "example_dc", "type": "float", "value": 1.0 } ]
where “type” field can be one of:
"boolean", "integer", "float", "string", "map"
Dynamic Configurations that are malformed, fail parsing, or otherwise error for any reason are included in the response and have their respective default values set:
"boolean" -> False "integer" -> 0 "float" -> 0.0 "string" -> "" "map" -> {}
-
Configuration Class¶
-
class
reddit_decider.
DeciderClient
(event_logger, prefix='experiments.', request_field_extractor=None)[source]¶ Configure a decider client.
This is meant to be used with
baseplate.Baseplate.configure_context()
.See
decider_client_from_config()
for available configuration settings.- Parameters
event_logger (
EventLogger
) – The EventLogger instance to be used to log bucketing events.prefix (
str
) – the prefix used to filter config keys (defaults to “experiments.”).request_field_extractor (
Optional
[Callable
[[RequestContext
],Dict
[str
,Union
[str
,int
,float
,bool
]]]]) – (optional) function used to populate fields such as"app_name"
&"build_number"
inDeciderContext()
that may be used for targeting
Configuration Function¶
-
reddit_decider.
decider_client_from_config
(app_config, event_logger, prefix='experiments.', request_field_extractor=None)[source]¶ Configure and return an
DeciderContextFactory
object.The keys used in your app’s
some_config.ini
file should be prefixed, e.g.experiments.path
, etc.Supported config keys:
path
(optional)The path to the experiment configuration file generated by the experiment configuration fetcher daemon. Defaults to
"/var/local/experiments.json"
.timeout
(optional)The time that we should wait for the file specified by
path
to exist. Defaults to blocking for30
seconds.backoff
(optional)The base amount of time for exponential backoff when trying to find the experiments config file. Defaults to no backoff between tries.
- Parameters
app_config (
Dict
[str
,str
]) – The application configuration which should have settings for the decider client.event_logger (
EventLogger
) – The EventLogger to be used to log bucketing events.prefix (
str
) – the prefix used to filter keys (defaults to “experiments.”).request_field_extractor (
Optional
[Callable
[[RequestContext
],Dict
[str
,Union
[str
,int
,float
,bool
]]]]) – (optional) function used to populate fields such as “app_name” & “build_number” in DeciderContext() that may be used for targeting
- Return type
Configuration Context Factory¶
-
class
reddit_decider.
DeciderContextFactory
(path, event_logger=None, timeout=None, backoff=None, request_field_extractor=None)[source]¶ Decider client context factory.
This factory will attach a new
reddit_decider.Decider
to an attribute on theRequestContext
.- Parameters
path (
str
) – Path to the experiment configuration file.event_logger (
Optional
[EventLogger
]) – The logger to use to log experiment eligibility events. If not provided, aDebugLogger
will be created and used.timeout (
Optional
[float
]) – How long, in seconds, to block instantiation waiting for the watched experiments file to become available (defaults to not blocking).backoff (
Optional
[float
]) – retry backoff time for experiments file watcher. Defaults to None, which is mapped to DEFAULT_FILEWATCHER_BACKOFF.request_field_extractor (
Optional
[Callable
[[RequestContext
],Dict
[str
,Union
[str
,int
,float
,bool
]]]]) – an optional function used to populate fields such as “app_name” & “build_number” in DeciderContext() that may be used for targeting