Skip to content
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: 4 additions & 0 deletions docs/command-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ rc paywalls delete <id> [--force] # attached/published pa

# Flows (paywalls, web funnels, and multi-step experiences)
rc flows list [--type paywall|web-funnel|general] [--exclude-type ...] # experimental (hidden from --help until Checkpoint rules roll out); repeatable; shows type and publish state
# Experiments — development v2 endpoints, normal discoverable CLI surface
rc experiments list [--status <status>]
rc experiments show <id> # human detail view shows variants, targeting, placements, and chronological timeline
rc experiments results <id> [--platform <p>] [--country <code>] [--exposure-status exposed|not_exposed] [--currency <code>] # human totals and decision signals; unknown platforms warn before passing through

# Rico (AI assistant)
rc rico [message] # streaming chat window in a TTY (--plain for a line loop)
Expand Down
14 changes: 7 additions & 7 deletions docs/previews/apps-list-all-projects.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions docs/previews/apps-list.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions docs/previews/experiments-list.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions docs/previews/experiments-results.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions docs/previews/experiments-show.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/specs/cli-coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,13 @@ endpoints:
path: /projects/{project_id}/audiences
- method: GET
path: /projects/{project_id}/audiences/{audience_id}
# Experiments (development-status; provided by the beta overlay)
- method: GET
path: /projects/{project_id}/experiments
- method: GET
path: /projects/{project_id}/experiments/{experiment_id}
- method: GET
path: /projects/{project_id}/experiments/{experiment_id}/results

# Audit
- method: GET
Expand Down
61 changes: 61 additions & 0 deletions docs/specs/v2-beta-overlay.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,67 @@ paths:
responses:
'200':
description: OK
/projects/{project_id}/experiments:
parameters:
- name: project_id
in: path
required: true
schema:
type: string
get:
summary: Get a list of experiments
operationId: list-experiments
x-release-status: development
x-revenuecat-release-gatekeeping: false
x-scopes:
- project_configuration:experiments:read
responses:
'200':
description: OK
/projects/{project_id}/experiments/{experiment_id}:
parameters:
- name: project_id
in: path
required: true
schema:
type: string
- name: experiment_id
in: path
required: true
schema:
type: string
get:
summary: Get an experiment
operationId: get-experiment
x-release-status: development
x-revenuecat-release-gatekeeping: false
x-scopes:
- project_configuration:experiments:read
responses:
'200':
description: OK
/projects/{project_id}/experiments/{experiment_id}/results:
parameters:
- name: project_id
in: path
required: true
schema:
type: string
- name: experiment_id
in: path
required: true
schema:
type: string
get:
summary: Get experiment results
operationId: get-experiment-results
x-release-status: development
x-revenuecat-release-gatekeeping: false
x-scopes:
- charts_metrics:experiment_results:read
responses:
'200':
description: OK
components:
schemas:
Currency:
Expand Down
4 changes: 3 additions & 1 deletion internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type Client struct {
Entitlements *EntitlementsService
Flows *FlowsService
Offerings *OfferingsService
Experiments *ExperimentsService
Packages *PackagesService
Products *ProductsService
Subscriptions *SubscriptionsService
Expand All @@ -72,7 +73,7 @@ type Client struct {
Apps *AppsService
StoreStatePlans *StoreStatePlansService
StoreState *StoreStateService
// add as we go: Discounts, Experiments, VirtualCurrencies catalog
// add as we go: Discounts, VirtualCurrencies catalog
}

func NewClient(opts Options) *Client {
Expand Down Expand Up @@ -103,6 +104,7 @@ func NewClient(opts Options) *Client {
c.Entitlements = &EntitlementsService{c: c}
c.Flows = &FlowsService{c: c}
c.Offerings = &OfferingsService{c: c}
c.Experiments = &ExperimentsService{c: c}
c.Packages = &PackagesService{c: c}
c.Products = &ProductsService{c: c}
c.Subscriptions = &SubscriptionsService{c: c}
Expand Down
221 changes: 221 additions & 0 deletions internal/api/experiments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package api

import (
"context"
"encoding/json"
"net/http"
"net/url"
"strconv"
)

type ExperimentsService struct{ c *Client }

type ExperimentOffering struct {
ID string `json:"id"`
Identifier string `json:"identifier,omitempty"`
DisplayName string `json:"display_name,omitempty"`
PaywallID string `json:"paywall_id,omitempty"`
}

type ExperimentCondition struct {
Field string `json:"field"`
Operator string `json:"operator"`
Value any `json:"value"`
Context any `json:"context,omitempty"`
}

type ExperimentPlacementOffering struct {
PlacementIdentifier string `json:"placement_identifier"`
OfferingA *ExperimentOffering `json:"offering_a,omitempty"`
OfferingB *ExperimentOffering `json:"offering_b,omitempty"`
OfferingC *ExperimentOffering `json:"offering_c,omitempty"`
OfferingD *ExperimentOffering `json:"offering_d,omitempty"`
}

type ExperimentPlacements struct {
FallbackOfferingA *ExperimentOffering `json:"fallback_offering_a,omitempty"`
FallbackOfferingB *ExperimentOffering `json:"fallback_offering_b,omitempty"`
FallbackOfferingC *ExperimentOffering `json:"fallback_offering_c,omitempty"`
FallbackOfferingD *ExperimentOffering `json:"fallback_offering_d,omitempty"`
PlacementOfferings []ExperimentPlacementOffering `json:"placement_offerings,omitempty"`
}

type ExperimentDurationSettings struct {
ConversionRatePercentage *float64 `json:"conversion_rate_percentage,omitempty"`
DailyEnrolledCustomers *int `json:"daily_enrolled_customers,omitempty"`
MinimumDetectableEffectPercentage *int `json:"minimum_detectable_effect_percentage,omitempty"`
ChanceToWinPercentage *int `json:"chance_to_win_percentage,omitempty"`
ConversionRateIsOverride *bool `json:"conversion_rate_percentage_is_override,omitempty"`
DailyEnrolledIsOverride *bool `json:"daily_enrolled_customers_is_override,omitempty"`
}

type ExperimentConflict struct {
ID string `json:"id"`
DisplayName string `json:"display_name,omitempty"`
}

type Experiment struct {
Object string `json:"object"`
ID string `json:"id"`
DisplayName string `json:"display_name"`
Status string `json:"status"`
CreatedAt Millis `json:"created_at"`
UpdatedAt Millis `json:"updated_at"`
StartedAt *Millis `json:"started_at,omitempty"`
StoppedAt *Millis `json:"stopped_at,omitempty"`
PausedAt *Millis `json:"paused_at,omitempty"`
ResumedAt *Millis `json:"resumed_at,omitempty"`
TotalRunningSeconds *int `json:"total_running_time_seconds,omitempty"`
EnrollmentPercent *int `json:"enrollment_percentage,omitempty"`
Priority *int `json:"priority,omitempty"`
OfferingA *ExperimentOffering `json:"offering_a,omitempty"`
OfferingB *ExperimentOffering `json:"offering_b,omitempty"`
OfferingC *ExperimentOffering `json:"offering_c,omitempty"`
OfferingD *ExperimentOffering `json:"offering_d,omitempty"`
Notes *string `json:"notes,omitempty"`
ExperimentType *string `json:"experiment_type,omitempty"`
PrimaryMetric *string `json:"primary_metric,omitempty"`
SecondaryMetrics []string `json:"secondary_metrics,omitempty"`
EnrollmentMode *string `json:"enrollment_mode,omitempty"`
AudienceID *string `json:"audience_id,omitempty"`
TargetingConditions []ExperimentCondition `json:"targeting_conditions,omitempty"`
PinnedAudience any `json:"pinned_audience,omitempty"`
DurationSettings *ExperimentDurationSettings `json:"experiment_duration_settings,omitempty"`
Placements *ExperimentPlacements `json:"placements,omitempty"`
Conflicts []ExperimentConflict `json:"conflicts,omitempty"`
}

type ListExperimentsOptions struct {
Status string
Limit int
StartingAfter string
}

func (s *ExperimentsService) List(ctx context.Context, projectID string, opts ListExperimentsOptions) (*Page[Experiment], error) {
q := url.Values{}
if opts.Status != "" {
q.Set("status", opts.Status)
}
if opts.Limit > 0 {
q.Set("limit", strconv.Itoa(opts.Limit))
}
if opts.StartingAfter != "" {
q.Set("starting_after", opts.StartingAfter)
}
path := pathExperiments(projectID)
if len(q) > 0 {
path += "?" + q.Encode()
}
var out Page[Experiment]
err := s.c.do(ctx, http.MethodGet, path, nil, &out)
return &out, err
}

func (s *ExperimentsService) Get(ctx context.Context, projectID, id string) (*Experiment, error) {
var out Experiment
err := s.c.do(ctx, http.MethodGet, pathExperiment(projectID, id), nil, &out)
return &out, err
}
Comment thread
joshdholtz marked this conversation as resolved.

func (s *ExperimentsService) GetWithPaywalls(ctx context.Context, projectID, id string) (*Experiment, error) {
var out Experiment
path := pathExperiment(projectID, id) + "?expand=offering.paywall"
err := s.c.do(ctx, http.MethodGet, path, nil, &out)
return &out, err
}

type ExperimentResults struct {
Object string `json:"object"`
Sections []ExperimentResultsSection `json:"sections"`
Currency string `json:"currency"`
Statistics []ExperimentResultStatistic `json:"statistics"`
PredictedLTV *ExperimentPredictedLTV `json:"predicted_ltv"`
}

type ExperimentResultsSection struct {
Object string `json:"object"`
Section string `json:"section"`
Description *string `json:"description,omitempty"`
Metrics []ExperimentResultMetric `json:"metrics"`
Segments []ExperimentResultSegment `json:"segments"`
Values []ExperimentResultValue `json:"values"`
}

type ExperimentResultMetric struct {
Name string `json:"name"`
Unit string `json:"unit,omitempty"`
Effect string `json:"effect,omitempty"`
}

type ExperimentResultSegment struct {
Object string `json:"object"`
ID string `json:"id"`
DisplayName string `json:"display_name"`
IsTotal bool `json:"is_total"`
}

type ExperimentResultValue struct {
Object string `json:"object"`
Metric int `json:"metric"`
Segment int `json:"segment"`
Variant string `json:"variant"`
Value *float64 `json:"value"`
Change *float64 `json:"change"`
CredibleInterval any `json:"credible_interval"`
LiftCredibleInterval any `json:"lift_credible_interval"`
}

type ExperimentResultStatistic struct {
Object string `json:"object"`
MetricName string `json:"metric_name"`
Variants []json.RawMessage `json:"variants"`
}

type ExperimentResultVariantStatistic struct {
Object string `json:"object"`
Name string `json:"name"`
ChanceToWin *float64 `json:"chance_to_win"`
ChanceToWinStatus string `json:"chance_to_win_status"`
CredibleIntervalLower *float64 `json:"credible_interval_lower"`
CredibleIntervalUpper *float64 `json:"credible_interval_upper"`
CredibleIntervalStatus string `json:"credible_interval_status"`
LiftCredibleIntervalLower *float64 `json:"lift_credible_interval_lower"`
LiftCredibleIntervalUpper *float64 `json:"lift_credible_interval_upper"`
LiftCredibleIntervalStatus string `json:"lift_credible_interval_status"`
}

type ExperimentPredictedLTV struct {
Object string `json:"object"`
PredictedWinnerVariant string `json:"predicted_winner_variant"`
Confidence int `json:"confidence"`
}

type ExperimentResultsOptions struct {
Platform string
Country string
ExposureStatus string
Currency string
}

func (s *ExperimentsService) Results(ctx context.Context, projectID, id string, opts ExperimentResultsOptions) (*ExperimentResults, error) {
q := url.Values{}
if opts.Platform != "" {
q.Set("platform", opts.Platform)
}
if opts.Country != "" {
q.Set("country", opts.Country)
}
if opts.ExposureStatus != "" {
q.Set("exposure_status", opts.ExposureStatus)
}
if opts.Currency != "" {
q.Set("currency", opts.Currency)
}
path := pathExperimentResults(projectID, id)
if len(q) > 0 {
path += "?" + q.Encode()
}
var out ExperimentResults
err := s.c.do(ctx, http.MethodGet, path, nil, &out)
return &out, err
}
Loading
Loading