diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Clients/AbtestingClient.cs b/clients/algoliasearch-client-csharp/algoliasearch/Clients/AbtestingClient.cs index adcffef9376..27f6e704f49 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Clients/AbtestingClient.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Clients/AbtestingClient.cs @@ -189,6 +189,36 @@ public interface IAbtestingClient /// ABTestResponse ABTestResponse DeleteABTest(int id, RequestOptions options = null, CancellationToken cancellationToken = default); + /// + /// Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + /// + /// + /// Required API Key ACLs: + /// - analytics + /// + /// Add extra http header or query parameters to Algolia. + /// Cancellation Token to cancel the request. + /// Thrown when arguments are not correct + /// Thrown when the API call was rejected by Algolia + /// Thrown when the client failed to call the endpoint + /// Task of EstimateABTestResponse + Task EstimateABTestAsync(EstimateABTestRequest estimateABTestRequest, RequestOptions options = null, CancellationToken cancellationToken = default); + + /// + /// Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. (Synchronous version) + /// + /// + /// Required API Key ACLs: + /// - analytics + /// + /// Add extra http header or query parameters to Algolia. + /// Cancellation Token to cancel the request. + /// Thrown when arguments are not correct + /// Thrown when the API call was rejected by Algolia + /// Thrown when the client failed to call the endpoint + /// EstimateABTestResponse + EstimateABTestResponse EstimateABTest(EstimateABTestRequest estimateABTestRequest, RequestOptions options = null, CancellationToken cancellationToken = default); + /// /// Retrieves the details for an A/B test by its ID. /// @@ -513,6 +543,26 @@ public ABTestResponse DeleteABTest(int id, RequestOptions options = null, Cancel AsyncHelper.RunSync(() => DeleteABTestAsync(id, options, cancellationToken)); + /// + public async Task EstimateABTestAsync(EstimateABTestRequest estimateABTestRequest, RequestOptions options = null, CancellationToken cancellationToken = default) + { + + if (estimateABTestRequest == null) + throw new ArgumentException("Parameter `estimateABTestRequest` is required when calling `EstimateABTest`."); + + var requestOptions = new InternalRequestOptions(options); + + + requestOptions.Data = estimateABTestRequest; + return await _transport.ExecuteRequestAsync(new HttpMethod("POST"), "/2/abtests/estimate", requestOptions, cancellationToken).ConfigureAwait(false); + } + + + /// + public EstimateABTestResponse EstimateABTest(EstimateABTestRequest estimateABTestRequest, RequestOptions options = null, CancellationToken cancellationToken = default) => + AsyncHelper.RunSync(() => EstimateABTestAsync(estimateABTestRequest, options, cancellationToken)); + + /// public async Task GetABTestAsync(int id, RequestOptions options = null, CancellationToken cancellationToken = default) { diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/Effect.cs b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EffectMetric.cs similarity index 92% rename from clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/Effect.cs rename to clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EffectMetric.cs index 4a367e65d2d..6606fefcd97 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/Effect.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EffectMetric.cs @@ -15,8 +15,8 @@ namespace Algolia.Search.Models.Abtesting; /// Metric for which you want to detect the smallest relative difference. /// /// Metric for which you want to detect the smallest relative difference. -[JsonConverter(typeof(Serializer.JsonStringEnumConverter))] -public enum Effect +[JsonConverter(typeof(Serializer.JsonStringEnumConverter))] +public enum EffectMetric { /// /// Enum AddToCartRate for value: addToCartRate diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EstimateABTestRequest.cs b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EstimateABTestRequest.cs new file mode 100644 index 00000000000..7678a10ff8e --- /dev/null +++ b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EstimateABTestRequest.cs @@ -0,0 +1,110 @@ +// +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +// +using System; +using System.Text; +using System.Linq; +using System.Text.Json.Serialization; +using System.Collections.Generic; +using Algolia.Search.Serializer; +using System.Text.Json; + +namespace Algolia.Search.Models.Abtesting; + +/// +/// EstimateABTestRequest +/// +public partial class EstimateABTestRequest +{ + /// + /// Initializes a new instance of the EstimateABTestRequest class. + /// + [JsonConstructor] + public EstimateABTestRequest() { } + /// + /// Initializes a new instance of the EstimateABTestRequest class. + /// + /// configuration (required). + /// A/B test variants. (required). + public EstimateABTestRequest(EstimateConfiguration configuration, List variants) + { + Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + Variants = variants ?? throw new ArgumentNullException(nameof(variants)); + } + + /// + /// Gets or Sets Configuration + /// + [JsonPropertyName("configuration")] + public EstimateConfiguration Configuration { get; set; } + + /// + /// A/B test variants. + /// + /// A/B test variants. + [JsonPropertyName("variants")] + public List Variants { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class EstimateABTestRequest {\n"); + sb.Append(" Configuration: ").Append(Configuration).Append("\n"); + sb.Append(" Variants: ").Append(Variants).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return JsonSerializer.Serialize(this, JsonConfig.Options); + } + + /// + /// Returns true if objects are equal + /// + /// Object to be compared + /// Boolean + public override bool Equals(object obj) + { + if (obj is not EstimateABTestRequest input) + { + return false; + } + + return + (Configuration == input.Configuration || (Configuration != null && Configuration.Equals(input.Configuration))) && + (Variants == input.Variants || Variants != null && input.Variants != null && Variants.SequenceEqual(input.Variants)); + } + + /// + /// Gets the hash code + /// + /// Hash code + public override int GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + int hashCode = 41; + if (Configuration != null) + { + hashCode = (hashCode * 59) + Configuration.GetHashCode(); + } + if (Variants != null) + { + hashCode = (hashCode * 59) + Variants.GetHashCode(); + } + return hashCode; + } + } + +} + diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EstimateABTestResponse.cs b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EstimateABTestResponse.cs new file mode 100644 index 00000000000..5f7d7e7ed64 --- /dev/null +++ b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EstimateABTestResponse.cs @@ -0,0 +1,106 @@ +// +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +// +using System; +using System.Text; +using System.Linq; +using System.Text.Json.Serialization; +using System.Collections.Generic; +using Algolia.Search.Serializer; +using System.Text.Json; + +namespace Algolia.Search.Models.Abtesting; + +/// +/// EstimateABTestResponse +/// +public partial class EstimateABTestResponse +{ + /// + /// Initializes a new instance of the EstimateABTestResponse class. + /// + public EstimateABTestResponse() + { + } + + /// + /// Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value is based on historical traffic. + /// + /// Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value is based on historical traffic. + [JsonPropertyName("durationDays")] + public long? DurationDays { get; set; } + + /// + /// Number of tracked searches needed to be able to detect the configured effect for the control variant. + /// + /// Number of tracked searches needed to be able to detect the configured effect for the control variant. + [JsonPropertyName("controlSampleSize")] + public long? ControlSampleSize { get; set; } + + /// + /// Number of tracked searches needed to be able to detect the configured effect for the experiment variant. + /// + /// Number of tracked searches needed to be able to detect the configured effect for the experiment variant. + [JsonPropertyName("experimentSampleSize")] + public long? ExperimentSampleSize { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class EstimateABTestResponse {\n"); + sb.Append(" DurationDays: ").Append(DurationDays).Append("\n"); + sb.Append(" ControlSampleSize: ").Append(ControlSampleSize).Append("\n"); + sb.Append(" ExperimentSampleSize: ").Append(ExperimentSampleSize).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return JsonSerializer.Serialize(this, JsonConfig.Options); + } + + /// + /// Returns true if objects are equal + /// + /// Object to be compared + /// Boolean + public override bool Equals(object obj) + { + if (obj is not EstimateABTestResponse input) + { + return false; + } + + return + (DurationDays == input.DurationDays || DurationDays.Equals(input.DurationDays)) && + (ControlSampleSize == input.ControlSampleSize || ControlSampleSize.Equals(input.ControlSampleSize)) && + (ExperimentSampleSize == input.ExperimentSampleSize || ExperimentSampleSize.Equals(input.ExperimentSampleSize)); + } + + /// + /// Gets the hash code + /// + /// Hash code + public override int GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + int hashCode = 41; + hashCode = (hashCode * 59) + DurationDays.GetHashCode(); + hashCode = (hashCode * 59) + ControlSampleSize.GetHashCode(); + hashCode = (hashCode * 59) + ExperimentSampleSize.GetHashCode(); + return hashCode; + } + } + +} + diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EstimateConfiguration.cs b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EstimateConfiguration.cs new file mode 100644 index 00000000000..5093dbf59a9 --- /dev/null +++ b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/EstimateConfiguration.cs @@ -0,0 +1,119 @@ +// +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +// +using System; +using System.Text; +using System.Linq; +using System.Text.Json.Serialization; +using System.Collections.Generic; +using Algolia.Search.Serializer; +using System.Text.Json; + +namespace Algolia.Search.Models.Abtesting; + +/// +/// A/B test configuration for estimating the sample size and duration using minimum detectable effect. +/// +public partial class EstimateConfiguration +{ + /// + /// Initializes a new instance of the EstimateConfiguration class. + /// + [JsonConstructor] + public EstimateConfiguration() { } + /// + /// Initializes a new instance of the EstimateConfiguration class. + /// + /// minimumDetectableEffect (required). + public EstimateConfiguration(MinimumDetectableEffect minimumDetectableEffect) + { + MinimumDetectableEffect = minimumDetectableEffect ?? throw new ArgumentNullException(nameof(minimumDetectableEffect)); + } + + /// + /// Gets or Sets Outliers + /// + [JsonPropertyName("outliers")] + public Outliers Outliers { get; set; } + + /// + /// Gets or Sets EmptySearch + /// + [JsonPropertyName("emptySearch")] + public EmptySearch EmptySearch { get; set; } + + /// + /// Gets or Sets MinimumDetectableEffect + /// + [JsonPropertyName("minimumDetectableEffect")] + public MinimumDetectableEffect MinimumDetectableEffect { get; set; } + + /// + /// Returns the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("class EstimateConfiguration {\n"); + sb.Append(" Outliers: ").Append(Outliers).Append("\n"); + sb.Append(" EmptySearch: ").Append(EmptySearch).Append("\n"); + sb.Append(" MinimumDetectableEffect: ").Append(MinimumDetectableEffect).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Returns the JSON string presentation of the object + /// + /// JSON string presentation of the object + public virtual string ToJson() + { + return JsonSerializer.Serialize(this, JsonConfig.Options); + } + + /// + /// Returns true if objects are equal + /// + /// Object to be compared + /// Boolean + public override bool Equals(object obj) + { + if (obj is not EstimateConfiguration input) + { + return false; + } + + return + (Outliers == input.Outliers || (Outliers != null && Outliers.Equals(input.Outliers))) && + (EmptySearch == input.EmptySearch || (EmptySearch != null && EmptySearch.Equals(input.EmptySearch))) && + (MinimumDetectableEffect == input.MinimumDetectableEffect || (MinimumDetectableEffect != null && MinimumDetectableEffect.Equals(input.MinimumDetectableEffect))); + } + + /// + /// Gets the hash code + /// + /// Hash code + public override int GetHashCode() + { + unchecked // Overflow is fine, just wrap + { + int hashCode = 41; + if (Outliers != null) + { + hashCode = (hashCode * 59) + Outliers.GetHashCode(); + } + if (EmptySearch != null) + { + hashCode = (hashCode * 59) + EmptySearch.GetHashCode(); + } + if (MinimumDetectableEffect != null) + { + hashCode = (hashCode * 59) + MinimumDetectableEffect.GetHashCode(); + } + return hashCode; + } + } + +} + diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/MinimumDetectableEffect.cs b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/MinimumDetectableEffect.cs index 9bb879f74fd..2c9930351fa 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/MinimumDetectableEffect.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Models/Abtesting/MinimumDetectableEffect.cs @@ -18,15 +18,24 @@ public partial class MinimumDetectableEffect { /// - /// Gets or Sets Effect + /// Gets or Sets Metric /// - [JsonPropertyName("effect")] - public Effect? Effect { get; set; } + [JsonPropertyName("metric")] + public EffectMetric? Metric { get; set; } /// /// Initializes a new instance of the MinimumDetectableEffect class. /// - public MinimumDetectableEffect() + [JsonConstructor] + public MinimumDetectableEffect() { } + /// + /// Initializes a new instance of the MinimumDetectableEffect class. + /// + /// Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. (required). + /// metric (required). + public MinimumDetectableEffect(double size, EffectMetric? metric) { + Size = size; + Metric = metric; } /// @@ -34,7 +43,7 @@ public MinimumDetectableEffect() /// /// Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. [JsonPropertyName("size")] - public double? Size { get; set; } + public double Size { get; set; } /// /// Returns the string presentation of the object @@ -45,7 +54,7 @@ public override string ToString() StringBuilder sb = new StringBuilder(); sb.Append("class MinimumDetectableEffect {\n"); sb.Append(" Size: ").Append(Size).Append("\n"); - sb.Append(" Effect: ").Append(Effect).Append("\n"); + sb.Append(" Metric: ").Append(Metric).Append("\n"); sb.Append("}\n"); return sb.ToString(); } @@ -73,7 +82,7 @@ public override bool Equals(object obj) return (Size == input.Size || Size.Equals(input.Size)) && - (Effect == input.Effect || Effect.Equals(input.Effect)); + (Metric == input.Metric || Metric.Equals(input.Metric)); } /// @@ -86,7 +95,7 @@ public override int GetHashCode() { int hashCode = 41; hashCode = (hashCode * 59) + Size.GetHashCode(); - hashCode = (hashCode * 59) + Effect.GetHashCode(); + hashCode = (hashCode * 59) + Metric.GetHashCode(); return hashCode; } } diff --git a/clients/algoliasearch-client-go/algolia/abtesting/api_abtesting.go b/clients/algoliasearch-client-go/algolia/abtesting/api_abtesting.go index 290b92519c2..bcf96d19c24 100644 --- a/clients/algoliasearch-client-go/algolia/abtesting/api_abtesting.go +++ b/clients/algoliasearch-client-go/algolia/abtesting/api_abtesting.go @@ -911,6 +911,135 @@ func (c *APIClient) DeleteABTest(r ApiDeleteABTestRequest, opts ...RequestOption return returnValue, nil } +func (r *ApiEstimateABTestRequest) UnmarshalJSON(b []byte) error { + req := map[string]json.RawMessage{} + err := json.Unmarshal(b, &req) + if err != nil { + return fmt.Errorf("cannot unmarshal request: %w", err) + } + if v, ok := req["estimateABTestRequest"]; ok { + err = json.Unmarshal(v, &r.estimateABTestRequest) + if err != nil { + err = json.Unmarshal(b, &r.estimateABTestRequest) + if err != nil { + return fmt.Errorf("cannot unmarshal estimateABTestRequest: %w", err) + } + } + } else { + err = json.Unmarshal(b, &r.estimateABTestRequest) + if err != nil { + return fmt.Errorf("cannot unmarshal body parameter estimateABTestRequest: %w", err) + } + } + + return nil +} + +// ApiEstimateABTestRequest represents the request with all the parameters for the API call. +type ApiEstimateABTestRequest struct { + estimateABTestRequest *EstimateABTestRequest +} + +// NewApiEstimateABTestRequest creates an instance of the ApiEstimateABTestRequest to be used for the API call. +func (c *APIClient) NewApiEstimateABTestRequest(estimateABTestRequest *EstimateABTestRequest) ApiEstimateABTestRequest { + return ApiEstimateABTestRequest{ + estimateABTestRequest: estimateABTestRequest, + } +} + +/* +EstimateABTest calls the API and returns the raw response from it. + + Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + + Required API Key ACLs: + - analytics + + Request can be constructed by NewApiEstimateABTestRequest with parameters below. + @param estimateABTestRequest EstimateABTestRequest + @param opts ...RequestOption - Optional parameters for the API call + @return *http.Response - The raw response from the API + @return []byte - The raw response body from the API + @return error - An error if the API call fails +*/ +func (c *APIClient) EstimateABTestWithHTTPInfo(r ApiEstimateABTestRequest, opts ...RequestOption) (*http.Response, []byte, error) { + requestPath := "/2/abtests/estimate" + + if r.estimateABTestRequest == nil { + return nil, nil, reportError("Parameter `estimateABTestRequest` is required when calling `EstimateABTest`.") + } + + conf := config{ + context: context.Background(), + queryParams: url.Values{}, + headerParams: map[string]string{}, + } + + // optional params if any + for _, opt := range opts { + opt.apply(&conf) + } + + var postBody any + + // body params + postBody = r.estimateABTestRequest + req, err := c.prepareRequest(conf.context, requestPath, http.MethodPost, postBody, conf.headerParams, conf.queryParams) + if err != nil { + return nil, nil, err + } + + return c.callAPI(req, false) +} + +/* +EstimateABTest casts the HTTP response body to a defined struct. + +Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + +Required API Key ACLs: + - analytics + +Request can be constructed by NewApiEstimateABTestRequest with parameters below. + + @param estimateABTestRequest EstimateABTestRequest + @return EstimateABTestResponse +*/ +func (c *APIClient) EstimateABTest(r ApiEstimateABTestRequest, opts ...RequestOption) (*EstimateABTestResponse, error) { + var returnValue *EstimateABTestResponse + + res, resBody, err := c.EstimateABTestWithHTTPInfo(r, opts...) + if err != nil { + return returnValue, err + } + if res == nil { + return returnValue, reportError("res is nil") + } + + if res.StatusCode >= 300 { + newErr := &APIError{ + Message: string(resBody), + Status: res.StatusCode, + } + + var v ErrorBase + err = c.decode(&v, resBody) + if err != nil { + newErr.Message = err.Error() + return returnValue, newErr + } + + return returnValue, newErr + } + + err = c.decode(&returnValue, resBody) + if err != nil { + return returnValue, reportError("cannot decode result: %w", err) + } + + return returnValue, nil +} + func (r *ApiGetABTestRequest) UnmarshalJSON(b []byte) error { req := map[string]json.RawMessage{} err := json.Unmarshal(b, &req) diff --git a/clients/algoliasearch-client-go/algolia/abtesting/model_effect.go b/clients/algoliasearch-client-go/algolia/abtesting/model_effect.go deleted file mode 100644 index 03965b7f6e0..00000000000 --- a/clients/algoliasearch-client-go/algolia/abtesting/model_effect.go +++ /dev/null @@ -1,69 +0,0 @@ -// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. -package abtesting - -import ( - "encoding/json" - "fmt" -) - -// Effect Metric for which you want to detect the smallest relative difference. -type Effect string - -// List of Effect. -const ( - EFFECT_ADD_TO_CART_RATE Effect = "addToCartRate" - EFFECT_CLICK_THROUGH_RATE Effect = "clickThroughRate" - EFFECT_CONVERSION_RATE Effect = "conversionRate" - EFFECT_PURCHASE_RATE Effect = "purchaseRate" -) - -// All allowed values of Effect enum. -var AllowedEffectEnumValues = []Effect{ - "addToCartRate", - "clickThroughRate", - "conversionRate", - "purchaseRate", -} - -func (v *Effect) UnmarshalJSON(src []byte) error { - var value string - err := json.Unmarshal(src, &value) - if err != nil { - return fmt.Errorf("failed to unmarshal value '%s' for enum 'Effect': %w", string(src), err) - } - enumTypeValue := Effect(value) - for _, existing := range AllowedEffectEnumValues { - if existing == enumTypeValue { - *v = enumTypeValue - return nil - } - } - - return fmt.Errorf("%+v is not a valid Effect", value) -} - -// NewEffectFromValue returns a pointer to a valid Effect -// for the value passed as argument, or an error if the value passed is not allowed by the enum. -func NewEffectFromValue(v string) (*Effect, error) { - ev := Effect(v) - if ev.IsValid() { - return &ev, nil - } else { - return nil, fmt.Errorf("invalid value '%v' for Effect: valid values are %v", v, AllowedEffectEnumValues) - } -} - -// IsValid return true if the value is valid for the enum, false otherwise. -func (v Effect) IsValid() bool { - for _, existing := range AllowedEffectEnumValues { - if existing == v { - return true - } - } - return false -} - -// Ptr returns reference to Effect value. -func (v Effect) Ptr() *Effect { - return &v -} diff --git a/clients/algoliasearch-client-go/algolia/abtesting/model_effect_metric.go b/clients/algoliasearch-client-go/algolia/abtesting/model_effect_metric.go new file mode 100644 index 00000000000..6928a31e06a --- /dev/null +++ b/clients/algoliasearch-client-go/algolia/abtesting/model_effect_metric.go @@ -0,0 +1,69 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +package abtesting + +import ( + "encoding/json" + "fmt" +) + +// EffectMetric Metric for which you want to detect the smallest relative difference. +type EffectMetric string + +// List of EffectMetric. +const ( + EFFECT_METRIC_ADD_TO_CART_RATE EffectMetric = "addToCartRate" + EFFECT_METRIC_CLICK_THROUGH_RATE EffectMetric = "clickThroughRate" + EFFECT_METRIC_CONVERSION_RATE EffectMetric = "conversionRate" + EFFECT_METRIC_PURCHASE_RATE EffectMetric = "purchaseRate" +) + +// All allowed values of EffectMetric enum. +var AllowedEffectMetricEnumValues = []EffectMetric{ + "addToCartRate", + "clickThroughRate", + "conversionRate", + "purchaseRate", +} + +func (v *EffectMetric) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return fmt.Errorf("failed to unmarshal value '%s' for enum 'EffectMetric': %w", string(src), err) + } + enumTypeValue := EffectMetric(value) + for _, existing := range AllowedEffectMetricEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid EffectMetric", value) +} + +// NewEffectMetricFromValue returns a pointer to a valid EffectMetric +// for the value passed as argument, or an error if the value passed is not allowed by the enum. +func NewEffectMetricFromValue(v string) (*EffectMetric, error) { + ev := EffectMetric(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for EffectMetric: valid values are %v", v, AllowedEffectMetricEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise. +func (v EffectMetric) IsValid() bool { + for _, existing := range AllowedEffectMetricEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to EffectMetric value. +func (v EffectMetric) Ptr() *EffectMetric { + return &v +} diff --git a/clients/algoliasearch-client-go/algolia/abtesting/model_estimate_ab_test_request.go b/clients/algoliasearch-client-go/algolia/abtesting/model_estimate_ab_test_request.go new file mode 100644 index 00000000000..e45f3002a95 --- /dev/null +++ b/clients/algoliasearch-client-go/algolia/abtesting/model_estimate_ab_test_request.go @@ -0,0 +1,103 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +package abtesting + +import ( + "encoding/json" + "fmt" +) + +// EstimateABTestRequest struct for EstimateABTestRequest. +type EstimateABTestRequest struct { + Configuration EstimateConfiguration `json:"configuration"` + // A/B test variants. + Variants []AddABTestsVariant `json:"variants"` +} + +// NewEstimateABTestRequest instantiates a new EstimateABTestRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed. +func NewEstimateABTestRequest(configuration EstimateConfiguration, variants []AddABTestsVariant) *EstimateABTestRequest { + this := &EstimateABTestRequest{} + this.Configuration = configuration + this.Variants = variants + return this +} + +// NewEmptyEstimateABTestRequest return a pointer to an empty EstimateABTestRequest object. +func NewEmptyEstimateABTestRequest() *EstimateABTestRequest { + return &EstimateABTestRequest{} +} + +// GetConfiguration returns the Configuration field value. +func (o *EstimateABTestRequest) GetConfiguration() EstimateConfiguration { + if o == nil { + var ret EstimateConfiguration + return ret + } + + return o.Configuration +} + +// GetConfigurationOk returns a tuple with the Configuration field value +// and a boolean to check if the value has been set. +func (o *EstimateABTestRequest) GetConfigurationOk() (*EstimateConfiguration, bool) { + if o == nil { + return nil, false + } + return &o.Configuration, true +} + +// SetConfiguration sets field value. +func (o *EstimateABTestRequest) SetConfiguration(v *EstimateConfiguration) *EstimateABTestRequest { + o.Configuration = *v + return o +} + +// GetVariants returns the Variants field value. +func (o *EstimateABTestRequest) GetVariants() []AddABTestsVariant { + if o == nil { + var ret []AddABTestsVariant + return ret + } + + return o.Variants +} + +// GetVariantsOk returns a tuple with the Variants field value +// and a boolean to check if the value has been set. +func (o *EstimateABTestRequest) GetVariantsOk() ([]AddABTestsVariant, bool) { + if o == nil { + return nil, false + } + return o.Variants, true +} + +// SetVariants sets field value. +func (o *EstimateABTestRequest) SetVariants(v []AddABTestsVariant) *EstimateABTestRequest { + o.Variants = v + return o +} + +func (o EstimateABTestRequest) MarshalJSON() ([]byte, error) { + toSerialize := map[string]any{} + if true { + toSerialize["configuration"] = o.Configuration + } + if true { + toSerialize["variants"] = o.Variants + } + serialized, err := json.Marshal(toSerialize) + if err != nil { + return nil, fmt.Errorf("failed to marshal EstimateABTestRequest: %w", err) + } + + return serialized, nil +} + +func (o EstimateABTestRequest) String() string { + out := "" + out += fmt.Sprintf(" configuration=%v\n", o.Configuration) + out += fmt.Sprintf(" variants=%v\n", o.Variants) + return fmt.Sprintf("EstimateABTestRequest {\n%s}", out) +} diff --git a/clients/algoliasearch-client-go/algolia/abtesting/model_estimate_ab_test_response.go b/clients/algoliasearch-client-go/algolia/abtesting/model_estimate_ab_test_response.go new file mode 100644 index 00000000000..d6613e92c63 --- /dev/null +++ b/clients/algoliasearch-client-go/algolia/abtesting/model_estimate_ab_test_response.go @@ -0,0 +1,180 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +package abtesting + +import ( + "encoding/json" + "fmt" +) + +// EstimateABTestResponse struct for EstimateABTestResponse. +type EstimateABTestResponse struct { + // Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value is based on historical traffic. + DurationDays *int64 `json:"durationDays,omitempty"` + // Number of tracked searches needed to be able to detect the configured effect for the control variant. + ControlSampleSize *int64 `json:"controlSampleSize,omitempty"` + // Number of tracked searches needed to be able to detect the configured effect for the experiment variant. + ExperimentSampleSize *int64 `json:"experimentSampleSize,omitempty"` +} + +type EstimateABTestResponseOption func(f *EstimateABTestResponse) + +func WithEstimateABTestResponseDurationDays(val int64) EstimateABTestResponseOption { + return func(f *EstimateABTestResponse) { + f.DurationDays = &val + } +} + +func WithEstimateABTestResponseControlSampleSize(val int64) EstimateABTestResponseOption { + return func(f *EstimateABTestResponse) { + f.ControlSampleSize = &val + } +} + +func WithEstimateABTestResponseExperimentSampleSize(val int64) EstimateABTestResponseOption { + return func(f *EstimateABTestResponse) { + f.ExperimentSampleSize = &val + } +} + +// NewEstimateABTestResponse instantiates a new EstimateABTestResponse object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed. +func NewEstimateABTestResponse(opts ...EstimateABTestResponseOption) *EstimateABTestResponse { + this := &EstimateABTestResponse{} + for _, opt := range opts { + opt(this) + } + return this +} + +// NewEmptyEstimateABTestResponse return a pointer to an empty EstimateABTestResponse object. +func NewEmptyEstimateABTestResponse() *EstimateABTestResponse { + return &EstimateABTestResponse{} +} + +// GetDurationDays returns the DurationDays field value if set, zero value otherwise. +func (o *EstimateABTestResponse) GetDurationDays() int64 { + if o == nil || o.DurationDays == nil { + var ret int64 + return ret + } + return *o.DurationDays +} + +// GetDurationDaysOk returns a tuple with the DurationDays field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *EstimateABTestResponse) GetDurationDaysOk() (*int64, bool) { + if o == nil || o.DurationDays == nil { + return nil, false + } + return o.DurationDays, true +} + +// HasDurationDays returns a boolean if a field has been set. +func (o *EstimateABTestResponse) HasDurationDays() bool { + if o != nil && o.DurationDays != nil { + return true + } + + return false +} + +// SetDurationDays gets a reference to the given int64 and assigns it to the DurationDays field. +func (o *EstimateABTestResponse) SetDurationDays(v int64) *EstimateABTestResponse { + o.DurationDays = &v + return o +} + +// GetControlSampleSize returns the ControlSampleSize field value if set, zero value otherwise. +func (o *EstimateABTestResponse) GetControlSampleSize() int64 { + if o == nil || o.ControlSampleSize == nil { + var ret int64 + return ret + } + return *o.ControlSampleSize +} + +// GetControlSampleSizeOk returns a tuple with the ControlSampleSize field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *EstimateABTestResponse) GetControlSampleSizeOk() (*int64, bool) { + if o == nil || o.ControlSampleSize == nil { + return nil, false + } + return o.ControlSampleSize, true +} + +// HasControlSampleSize returns a boolean if a field has been set. +func (o *EstimateABTestResponse) HasControlSampleSize() bool { + if o != nil && o.ControlSampleSize != nil { + return true + } + + return false +} + +// SetControlSampleSize gets a reference to the given int64 and assigns it to the ControlSampleSize field. +func (o *EstimateABTestResponse) SetControlSampleSize(v int64) *EstimateABTestResponse { + o.ControlSampleSize = &v + return o +} + +// GetExperimentSampleSize returns the ExperimentSampleSize field value if set, zero value otherwise. +func (o *EstimateABTestResponse) GetExperimentSampleSize() int64 { + if o == nil || o.ExperimentSampleSize == nil { + var ret int64 + return ret + } + return *o.ExperimentSampleSize +} + +// GetExperimentSampleSizeOk returns a tuple with the ExperimentSampleSize field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *EstimateABTestResponse) GetExperimentSampleSizeOk() (*int64, bool) { + if o == nil || o.ExperimentSampleSize == nil { + return nil, false + } + return o.ExperimentSampleSize, true +} + +// HasExperimentSampleSize returns a boolean if a field has been set. +func (o *EstimateABTestResponse) HasExperimentSampleSize() bool { + if o != nil && o.ExperimentSampleSize != nil { + return true + } + + return false +} + +// SetExperimentSampleSize gets a reference to the given int64 and assigns it to the ExperimentSampleSize field. +func (o *EstimateABTestResponse) SetExperimentSampleSize(v int64) *EstimateABTestResponse { + o.ExperimentSampleSize = &v + return o +} + +func (o EstimateABTestResponse) MarshalJSON() ([]byte, error) { + toSerialize := map[string]any{} + if o.DurationDays != nil { + toSerialize["durationDays"] = o.DurationDays + } + if o.ControlSampleSize != nil { + toSerialize["controlSampleSize"] = o.ControlSampleSize + } + if o.ExperimentSampleSize != nil { + toSerialize["experimentSampleSize"] = o.ExperimentSampleSize + } + serialized, err := json.Marshal(toSerialize) + if err != nil { + return nil, fmt.Errorf("failed to marshal EstimateABTestResponse: %w", err) + } + + return serialized, nil +} + +func (o EstimateABTestResponse) String() string { + out := "" + out += fmt.Sprintf(" durationDays=%v\n", o.DurationDays) + out += fmt.Sprintf(" controlSampleSize=%v\n", o.ControlSampleSize) + out += fmt.Sprintf(" experimentSampleSize=%v\n", o.ExperimentSampleSize) + return fmt.Sprintf("EstimateABTestResponse {\n%s}", out) +} diff --git a/clients/algoliasearch-client-go/algolia/abtesting/model_estimate_configuration.go b/clients/algoliasearch-client-go/algolia/abtesting/model_estimate_configuration.go new file mode 100644 index 00000000000..4bf95184c6d --- /dev/null +++ b/clients/algoliasearch-client-go/algolia/abtesting/model_estimate_configuration.go @@ -0,0 +1,164 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +package abtesting + +import ( + "encoding/json" + "fmt" +) + +// EstimateConfiguration A/B test configuration for estimating the sample size and duration using minimum detectable effect. +type EstimateConfiguration struct { + Outliers *Outliers `json:"outliers,omitempty"` + EmptySearch *EmptySearch `json:"emptySearch,omitempty"` + MinimumDetectableEffect MinimumDetectableEffect `json:"minimumDetectableEffect"` +} + +type EstimateConfigurationOption func(f *EstimateConfiguration) + +func WithEstimateConfigurationOutliers(val Outliers) EstimateConfigurationOption { + return func(f *EstimateConfiguration) { + f.Outliers = &val + } +} + +func WithEstimateConfigurationEmptySearch(val EmptySearch) EstimateConfigurationOption { + return func(f *EstimateConfiguration) { + f.EmptySearch = &val + } +} + +// NewEstimateConfiguration instantiates a new EstimateConfiguration object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed. +func NewEstimateConfiguration(minimumDetectableEffect MinimumDetectableEffect, opts ...EstimateConfigurationOption) *EstimateConfiguration { + this := &EstimateConfiguration{} + this.MinimumDetectableEffect = minimumDetectableEffect + for _, opt := range opts { + opt(this) + } + return this +} + +// NewEmptyEstimateConfiguration return a pointer to an empty EstimateConfiguration object. +func NewEmptyEstimateConfiguration() *EstimateConfiguration { + return &EstimateConfiguration{} +} + +// GetOutliers returns the Outliers field value if set, zero value otherwise. +func (o *EstimateConfiguration) GetOutliers() Outliers { + if o == nil || o.Outliers == nil { + var ret Outliers + return ret + } + return *o.Outliers +} + +// GetOutliersOk returns a tuple with the Outliers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *EstimateConfiguration) GetOutliersOk() (*Outliers, bool) { + if o == nil || o.Outliers == nil { + return nil, false + } + return o.Outliers, true +} + +// HasOutliers returns a boolean if a field has been set. +func (o *EstimateConfiguration) HasOutliers() bool { + if o != nil && o.Outliers != nil { + return true + } + + return false +} + +// SetOutliers gets a reference to the given Outliers and assigns it to the Outliers field. +func (o *EstimateConfiguration) SetOutliers(v *Outliers) *EstimateConfiguration { + o.Outliers = v + return o +} + +// GetEmptySearch returns the EmptySearch field value if set, zero value otherwise. +func (o *EstimateConfiguration) GetEmptySearch() EmptySearch { + if o == nil || o.EmptySearch == nil { + var ret EmptySearch + return ret + } + return *o.EmptySearch +} + +// GetEmptySearchOk returns a tuple with the EmptySearch field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *EstimateConfiguration) GetEmptySearchOk() (*EmptySearch, bool) { + if o == nil || o.EmptySearch == nil { + return nil, false + } + return o.EmptySearch, true +} + +// HasEmptySearch returns a boolean if a field has been set. +func (o *EstimateConfiguration) HasEmptySearch() bool { + if o != nil && o.EmptySearch != nil { + return true + } + + return false +} + +// SetEmptySearch gets a reference to the given EmptySearch and assigns it to the EmptySearch field. +func (o *EstimateConfiguration) SetEmptySearch(v *EmptySearch) *EstimateConfiguration { + o.EmptySearch = v + return o +} + +// GetMinimumDetectableEffect returns the MinimumDetectableEffect field value. +func (o *EstimateConfiguration) GetMinimumDetectableEffect() MinimumDetectableEffect { + if o == nil { + var ret MinimumDetectableEffect + return ret + } + + return o.MinimumDetectableEffect +} + +// GetMinimumDetectableEffectOk returns a tuple with the MinimumDetectableEffect field value +// and a boolean to check if the value has been set. +func (o *EstimateConfiguration) GetMinimumDetectableEffectOk() (*MinimumDetectableEffect, bool) { + if o == nil { + return nil, false + } + return &o.MinimumDetectableEffect, true +} + +// SetMinimumDetectableEffect sets field value. +func (o *EstimateConfiguration) SetMinimumDetectableEffect(v *MinimumDetectableEffect) *EstimateConfiguration { + o.MinimumDetectableEffect = *v + return o +} + +func (o EstimateConfiguration) MarshalJSON() ([]byte, error) { + toSerialize := map[string]any{} + if o.Outliers != nil { + toSerialize["outliers"] = o.Outliers + } + if o.EmptySearch != nil { + toSerialize["emptySearch"] = o.EmptySearch + } + if true { + toSerialize["minimumDetectableEffect"] = o.MinimumDetectableEffect + } + serialized, err := json.Marshal(toSerialize) + if err != nil { + return nil, fmt.Errorf("failed to marshal EstimateConfiguration: %w", err) + } + + return serialized, nil +} + +func (o EstimateConfiguration) String() string { + out := "" + out += fmt.Sprintf(" outliers=%v\n", o.Outliers) + out += fmt.Sprintf(" emptySearch=%v\n", o.EmptySearch) + out += fmt.Sprintf(" minimumDetectableEffect=%v\n", o.MinimumDetectableEffect) + return fmt.Sprintf("EstimateConfiguration {\n%s}", out) +} diff --git a/clients/algoliasearch-client-go/algolia/abtesting/model_minimum_detectable_effect.go b/clients/algoliasearch-client-go/algolia/abtesting/model_minimum_detectable_effect.go index 7bab5dc57ef..a73900199e4 100644 --- a/clients/algoliasearch-client-go/algolia/abtesting/model_minimum_detectable_effect.go +++ b/clients/algoliasearch-client-go/algolia/abtesting/model_minimum_detectable_effect.go @@ -9,33 +9,18 @@ import ( // MinimumDetectableEffect Configuration for the smallest difference between test variants you want to detect. type MinimumDetectableEffect struct { // Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. - Size *float64 `json:"size,omitempty"` - Effect *Effect `json:"effect,omitempty"` -} - -type MinimumDetectableEffectOption func(f *MinimumDetectableEffect) - -func WithMinimumDetectableEffectSize(val float64) MinimumDetectableEffectOption { - return func(f *MinimumDetectableEffect) { - f.Size = &val - } -} - -func WithMinimumDetectableEffectEffect(val Effect) MinimumDetectableEffectOption { - return func(f *MinimumDetectableEffect) { - f.Effect = &val - } + Size float64 `json:"size"` + Metric EffectMetric `json:"metric"` } // NewMinimumDetectableEffect instantiates a new MinimumDetectableEffect object // This constructor will assign default values to properties that have it defined, // and makes sure properties required by API are set, but the set of arguments // will change when the set of required properties is changed. -func NewMinimumDetectableEffect(opts ...MinimumDetectableEffectOption) *MinimumDetectableEffect { +func NewMinimumDetectableEffect(size float64, metric EffectMetric) *MinimumDetectableEffect { this := &MinimumDetectableEffect{} - for _, opt := range opts { - opt(this) - } + this.Size = size + this.Metric = metric return this } @@ -44,79 +29,63 @@ func NewEmptyMinimumDetectableEffect() *MinimumDetectableEffect { return &MinimumDetectableEffect{} } -// GetSize returns the Size field value if set, zero value otherwise. +// GetSize returns the Size field value. func (o *MinimumDetectableEffect) GetSize() float64 { - if o == nil || o.Size == nil { + if o == nil { var ret float64 return ret } - return *o.Size + + return o.Size } -// GetSizeOk returns a tuple with the Size field value if set, nil otherwise +// GetSizeOk returns a tuple with the Size field value // and a boolean to check if the value has been set. func (o *MinimumDetectableEffect) GetSizeOk() (*float64, bool) { - if o == nil || o.Size == nil { + if o == nil { return nil, false } - return o.Size, true + return &o.Size, true } -// HasSize returns a boolean if a field has been set. -func (o *MinimumDetectableEffect) HasSize() bool { - if o != nil && o.Size != nil { - return true - } - - return false -} - -// SetSize gets a reference to the given float64 and assigns it to the Size field. +// SetSize sets field value. func (o *MinimumDetectableEffect) SetSize(v float64) *MinimumDetectableEffect { - o.Size = &v + o.Size = v return o } -// GetEffect returns the Effect field value if set, zero value otherwise. -func (o *MinimumDetectableEffect) GetEffect() Effect { - if o == nil || o.Effect == nil { - var ret Effect +// GetMetric returns the Metric field value. +func (o *MinimumDetectableEffect) GetMetric() EffectMetric { + if o == nil { + var ret EffectMetric return ret } - return *o.Effect + + return o.Metric } -// GetEffectOk returns a tuple with the Effect field value if set, nil otherwise +// GetMetricOk returns a tuple with the Metric field value // and a boolean to check if the value has been set. -func (o *MinimumDetectableEffect) GetEffectOk() (*Effect, bool) { - if o == nil || o.Effect == nil { +func (o *MinimumDetectableEffect) GetMetricOk() (*EffectMetric, bool) { + if o == nil { return nil, false } - return o.Effect, true -} - -// HasEffect returns a boolean if a field has been set. -func (o *MinimumDetectableEffect) HasEffect() bool { - if o != nil && o.Effect != nil { - return true - } - - return false + return &o.Metric, true } -// SetEffect gets a reference to the given Effect and assigns it to the Effect field. -func (o *MinimumDetectableEffect) SetEffect(v Effect) *MinimumDetectableEffect { - o.Effect = &v +// SetMetric sets field value. +func (o *MinimumDetectableEffect) SetMetric(v EffectMetric) *MinimumDetectableEffect { + o.Metric = v return o } func (o MinimumDetectableEffect) MarshalJSON() ([]byte, error) { toSerialize := map[string]any{} - if o.Size != nil { + if true { toSerialize["size"] = o.Size } - if o.Effect != nil { - toSerialize["effect"] = o.Effect + if true { + toSerialize["metric"] = o.Metric } serialized, err := json.Marshal(toSerialize) if err != nil { @@ -129,6 +98,6 @@ func (o MinimumDetectableEffect) MarshalJSON() ([]byte, error) { func (o MinimumDetectableEffect) String() string { out := "" out += fmt.Sprintf(" size=%v\n", o.Size) - out += fmt.Sprintf(" effect=%v\n", o.Effect) + out += fmt.Sprintf(" metric=%v\n", o.Metric) return fmt.Sprintf("MinimumDetectableEffect {\n%s}", out) } diff --git a/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/api/AbtestingClient.java b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/api/AbtestingClient.java index e12cf180ab2..5592d2e3227 100644 --- a/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/api/AbtestingClient.java +++ b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/api/AbtestingClient.java @@ -574,6 +574,62 @@ public CompletableFuture deleteABTestAsync(@Nonnull Integer id) return this.deleteABTestAsync(id, null); } + /** + * Given the traffic percentage and the expected effect size, this endpoint estimates the sample + * size and duration of an A/B test based on historical traffic. + * + * @param estimateABTestRequest (required) + * @param requestOptions The requestOptions to send along with the query, they will be merged with + * the transporter requestOptions. + * @throws AlgoliaRuntimeException If it fails to process the API call + */ + public EstimateABTestResponse estimateABTest(@Nonnull EstimateABTestRequest estimateABTestRequest, RequestOptions requestOptions) + throws AlgoliaRuntimeException { + return LaunderThrowable.await(estimateABTestAsync(estimateABTestRequest, requestOptions)); + } + + /** + * Given the traffic percentage and the expected effect size, this endpoint estimates the sample + * size and duration of an A/B test based on historical traffic. + * + * @param estimateABTestRequest (required) + * @throws AlgoliaRuntimeException If it fails to process the API call + */ + public EstimateABTestResponse estimateABTest(@Nonnull EstimateABTestRequest estimateABTestRequest) throws AlgoliaRuntimeException { + return this.estimateABTest(estimateABTestRequest, null); + } + + /** + * (asynchronously) Given the traffic percentage and the expected effect size, this endpoint + * estimates the sample size and duration of an A/B test based on historical traffic. + * + * @param estimateABTestRequest (required) + * @param requestOptions The requestOptions to send along with the query, they will be merged with + * the transporter requestOptions. + * @throws AlgoliaRuntimeException If it fails to process the API call + */ + public CompletableFuture estimateABTestAsync( + @Nonnull EstimateABTestRequest estimateABTestRequest, + RequestOptions requestOptions + ) throws AlgoliaRuntimeException { + Parameters.requireNonNull(estimateABTestRequest, "Parameter `estimateABTestRequest` is required when calling `estimateABTest`."); + + HttpRequest request = HttpRequest.builder().setPath("/2/abtests/estimate").setMethod("POST").setBody(estimateABTestRequest).build(); + return executeAsync(request, requestOptions, new TypeReference() {}); + } + + /** + * (asynchronously) Given the traffic percentage and the expected effect size, this endpoint + * estimates the sample size and duration of an A/B test based on historical traffic. + * + * @param estimateABTestRequest (required) + * @throws AlgoliaRuntimeException If it fails to process the API call + */ + public CompletableFuture estimateABTestAsync(@Nonnull EstimateABTestRequest estimateABTestRequest) + throws AlgoliaRuntimeException { + return this.estimateABTestAsync(estimateABTestRequest, null); + } + /** * Retrieves the details for an A/B test by its ID. * diff --git a/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/Effect.java b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EffectMetric.java similarity index 84% rename from clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/Effect.java rename to clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EffectMetric.java index c21382c569c..cf81969f90b 100644 --- a/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/Effect.java +++ b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EffectMetric.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.annotation.*; /** Metric for which you want to detect the smallest relative difference. */ -public enum Effect { +public enum EffectMetric { ADD_TO_CART_RATE("addToCartRate"), CLICK_THROUGH_RATE("clickThroughRate"), @@ -18,7 +18,7 @@ public enum Effect { private final String value; - Effect(String value) { + EffectMetric(String value) { this.value = value; } @@ -33,8 +33,8 @@ public String toString() { } @JsonCreator - public static Effect fromValue(String value) { - for (Effect b : Effect.values()) { + public static EffectMetric fromValue(String value) { + for (EffectMetric b : EffectMetric.values()) { if (b.value.equals(value)) { return b; } diff --git a/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EstimateABTestRequest.java b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EstimateABTestRequest.java new file mode 100644 index 00000000000..a84e697804a --- /dev/null +++ b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EstimateABTestRequest.java @@ -0,0 +1,87 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost +// - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +package com.algolia.model.abtesting; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** EstimateABTestRequest */ +public class EstimateABTestRequest { + + @JsonProperty("configuration") + private EstimateConfiguration configuration; + + @JsonProperty("variants") + private List variants = new ArrayList<>(); + + public EstimateABTestRequest setConfiguration(EstimateConfiguration configuration) { + this.configuration = configuration; + return this; + } + + /** Get configuration */ + @javax.annotation.Nonnull + public EstimateConfiguration getConfiguration() { + return configuration; + } + + public EstimateABTestRequest setVariants(List variants) { + this.variants = variants; + return this; + } + + public EstimateABTestRequest addVariants(AddABTestsVariant variantsItem) { + this.variants.add(variantsItem); + return this; + } + + /** A/B test variants. */ + @javax.annotation.Nonnull + public List getVariants() { + return variants; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EstimateABTestRequest estimateABTestRequest = (EstimateABTestRequest) o; + return ( + Objects.equals(this.configuration, estimateABTestRequest.configuration) && + Objects.equals(this.variants, estimateABTestRequest.variants) + ); + } + + @Override + public int hashCode() { + return Objects.hash(configuration, variants); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class EstimateABTestRequest {\n"); + sb.append(" configuration: ").append(toIndentedString(configuration)).append("\n"); + sb.append(" variants: ").append(toIndentedString(variants)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EstimateABTestResponse.java b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EstimateABTestResponse.java new file mode 100644 index 00000000000..3b6f670bd99 --- /dev/null +++ b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EstimateABTestResponse.java @@ -0,0 +1,105 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost +// - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +package com.algolia.model.abtesting; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.*; +import java.util.Objects; + +/** EstimateABTestResponse */ +public class EstimateABTestResponse { + + @JsonProperty("durationDays") + private Long durationDays; + + @JsonProperty("controlSampleSize") + private Long controlSampleSize; + + @JsonProperty("experimentSampleSize") + private Long experimentSampleSize; + + public EstimateABTestResponse setDurationDays(Long durationDays) { + this.durationDays = durationDays; + return this; + } + + /** + * Estimated number of days needed to reach the sample sizes required for detecting the configured + * effect. This value is based on historical traffic. + */ + @javax.annotation.Nullable + public Long getDurationDays() { + return durationDays; + } + + public EstimateABTestResponse setControlSampleSize(Long controlSampleSize) { + this.controlSampleSize = controlSampleSize; + return this; + } + + /** + * Number of tracked searches needed to be able to detect the configured effect for the control + * variant. + */ + @javax.annotation.Nullable + public Long getControlSampleSize() { + return controlSampleSize; + } + + public EstimateABTestResponse setExperimentSampleSize(Long experimentSampleSize) { + this.experimentSampleSize = experimentSampleSize; + return this; + } + + /** + * Number of tracked searches needed to be able to detect the configured effect for the experiment + * variant. + */ + @javax.annotation.Nullable + public Long getExperimentSampleSize() { + return experimentSampleSize; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EstimateABTestResponse estimateABTestResponse = (EstimateABTestResponse) o; + return ( + Objects.equals(this.durationDays, estimateABTestResponse.durationDays) && + Objects.equals(this.controlSampleSize, estimateABTestResponse.controlSampleSize) && + Objects.equals(this.experimentSampleSize, estimateABTestResponse.experimentSampleSize) + ); + } + + @Override + public int hashCode() { + return Objects.hash(durationDays, controlSampleSize, experimentSampleSize); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class EstimateABTestResponse {\n"); + sb.append(" durationDays: ").append(toIndentedString(durationDays)).append("\n"); + sb.append(" controlSampleSize: ").append(toIndentedString(controlSampleSize)).append("\n"); + sb.append(" experimentSampleSize: ").append(toIndentedString(experimentSampleSize)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EstimateConfiguration.java b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EstimateConfiguration.java new file mode 100644 index 00000000000..0e95fddb900 --- /dev/null +++ b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/EstimateConfiguration.java @@ -0,0 +1,99 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost +// - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +package com.algolia.model.abtesting; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.*; +import java.util.Objects; + +/** + * A/B test configuration for estimating the sample size and duration using minimum detectable + * effect. + */ +public class EstimateConfiguration { + + @JsonProperty("outliers") + private Outliers outliers; + + @JsonProperty("emptySearch") + private EmptySearch emptySearch; + + @JsonProperty("minimumDetectableEffect") + private MinimumDetectableEffect minimumDetectableEffect; + + public EstimateConfiguration setOutliers(Outliers outliers) { + this.outliers = outliers; + return this; + } + + /** Get outliers */ + @javax.annotation.Nullable + public Outliers getOutliers() { + return outliers; + } + + public EstimateConfiguration setEmptySearch(EmptySearch emptySearch) { + this.emptySearch = emptySearch; + return this; + } + + /** Get emptySearch */ + @javax.annotation.Nullable + public EmptySearch getEmptySearch() { + return emptySearch; + } + + public EstimateConfiguration setMinimumDetectableEffect(MinimumDetectableEffect minimumDetectableEffect) { + this.minimumDetectableEffect = minimumDetectableEffect; + return this; + } + + /** Get minimumDetectableEffect */ + @javax.annotation.Nonnull + public MinimumDetectableEffect getMinimumDetectableEffect() { + return minimumDetectableEffect; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + EstimateConfiguration estimateConfiguration = (EstimateConfiguration) o; + return ( + Objects.equals(this.outliers, estimateConfiguration.outliers) && + Objects.equals(this.emptySearch, estimateConfiguration.emptySearch) && + Objects.equals(this.minimumDetectableEffect, estimateConfiguration.minimumDetectableEffect) + ); + } + + @Override + public int hashCode() { + return Objects.hash(outliers, emptySearch, minimumDetectableEffect); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class EstimateConfiguration {\n"); + sb.append(" outliers: ").append(toIndentedString(outliers)).append("\n"); + sb.append(" emptySearch: ").append(toIndentedString(emptySearch)).append("\n"); + sb.append(" minimumDetectableEffect: ").append(toIndentedString(minimumDetectableEffect)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/MinimumDetectableEffect.java b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/MinimumDetectableEffect.java index 202ac09fe10..bd4cd428b90 100644 --- a/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/MinimumDetectableEffect.java +++ b/clients/algoliasearch-client-java/algoliasearch/src/main/java/com/algolia/model/abtesting/MinimumDetectableEffect.java @@ -13,8 +13,8 @@ public class MinimumDetectableEffect { @JsonProperty("size") private Double size; - @JsonProperty("effect") - private Effect effect; + @JsonProperty("metric") + private EffectMetric metric; public MinimumDetectableEffect setSize(Double size) { this.size = size; @@ -25,20 +25,20 @@ public MinimumDetectableEffect setSize(Double size) { * Smallest difference in an observable metric between variants. For example, to detect a 10% * difference between variants, set this value to 0.1. minimum: 0 maximum: 1 */ - @javax.annotation.Nullable + @javax.annotation.Nonnull public Double getSize() { return size; } - public MinimumDetectableEffect setEffect(Effect effect) { - this.effect = effect; + public MinimumDetectableEffect setMetric(EffectMetric metric) { + this.metric = metric; return this; } - /** Get effect */ - @javax.annotation.Nullable - public Effect getEffect() { - return effect; + /** Get metric */ + @javax.annotation.Nonnull + public EffectMetric getMetric() { + return metric; } @Override @@ -50,12 +50,12 @@ public boolean equals(Object o) { return false; } MinimumDetectableEffect minimumDetectableEffect = (MinimumDetectableEffect) o; - return Objects.equals(this.size, minimumDetectableEffect.size) && Objects.equals(this.effect, minimumDetectableEffect.effect); + return Objects.equals(this.size, minimumDetectableEffect.size) && Objects.equals(this.metric, minimumDetectableEffect.metric); } @Override public int hashCode() { - return Objects.hash(size, effect); + return Objects.hash(size, metric); } @Override @@ -63,7 +63,7 @@ public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class MinimumDetectableEffect {\n"); sb.append(" size: ").append(toIndentedString(size)).append("\n"); - sb.append(" effect: ").append(toIndentedString(effect)).append("\n"); + sb.append(" metric: ").append(toIndentedString(metric)).append("\n"); sb.append("}"); return sb.toString(); } diff --git a/clients/algoliasearch-client-javascript/packages/client-abtesting/model/effect.ts b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/effectMetric.ts similarity index 72% rename from clients/algoliasearch-client-javascript/packages/client-abtesting/model/effect.ts rename to clients/algoliasearch-client-javascript/packages/client-abtesting/model/effectMetric.ts index b884ebfb125..dc7e6ee7796 100644 --- a/clients/algoliasearch-client-javascript/packages/client-abtesting/model/effect.ts +++ b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/effectMetric.ts @@ -3,4 +3,4 @@ /** * Metric for which you want to detect the smallest relative difference. */ -export type Effect = 'addToCartRate' | 'clickThroughRate' | 'conversionRate' | 'purchaseRate'; +export type EffectMetric = 'addToCartRate' | 'clickThroughRate' | 'conversionRate' | 'purchaseRate'; diff --git a/clients/algoliasearch-client-javascript/packages/client-abtesting/model/estimateABTestRequest.ts b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/estimateABTestRequest.ts new file mode 100644 index 00000000000..1c5e6c3dbaa --- /dev/null +++ b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/estimateABTestRequest.ts @@ -0,0 +1,13 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +import type { AddABTestsVariant } from './addABTestsVariant'; +import type { EstimateConfiguration } from './estimateConfiguration'; + +export type EstimateABTestRequest = { + configuration: EstimateConfiguration; + + /** + * A/B test variants. + */ + variants: Array; +}; diff --git a/clients/algoliasearch-client-javascript/packages/client-abtesting/model/estimateABTestResponse.ts b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/estimateABTestResponse.ts new file mode 100644 index 00000000000..edc4f7c304e --- /dev/null +++ b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/estimateABTestResponse.ts @@ -0,0 +1,18 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +export type EstimateABTestResponse = { + /** + * Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value is based on historical traffic. + */ + durationDays?: number; + + /** + * Number of tracked searches needed to be able to detect the configured effect for the control variant. + */ + controlSampleSize?: number; + + /** + * Number of tracked searches needed to be able to detect the configured effect for the experiment variant. + */ + experimentSampleSize?: number; +}; diff --git a/clients/algoliasearch-client-javascript/packages/client-abtesting/model/estimateConfiguration.ts b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/estimateConfiguration.ts new file mode 100644 index 00000000000..ee60c3c509f --- /dev/null +++ b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/estimateConfiguration.ts @@ -0,0 +1,16 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +import type { EmptySearch } from './emptySearch'; +import type { MinimumDetectableEffect } from './minimumDetectableEffect'; +import type { Outliers } from './outliers'; + +/** + * A/B test configuration for estimating the sample size and duration using minimum detectable effect. + */ +export type EstimateConfiguration = { + outliers?: Outliers; + + emptySearch?: EmptySearch; + + minimumDetectableEffect: MinimumDetectableEffect; +}; diff --git a/clients/algoliasearch-client-javascript/packages/client-abtesting/model/index.ts b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/index.ts index 8af7fe70c87..980f1f4bcad 100644 --- a/clients/algoliasearch-client-javascript/packages/client-abtesting/model/index.ts +++ b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/index.ts @@ -10,10 +10,13 @@ export * from './addABTestsVariant'; export * from './clientMethodProps'; export * from './currency'; export * from './customSearchParams'; -export * from './effect'; +export * from './effectMetric'; export * from './emptySearch'; export * from './emptySearchFilter'; export * from './errorBase'; +export * from './estimateABTestRequest'; +export * from './estimateABTestResponse'; +export * from './estimateConfiguration'; export * from './filterEffects'; export * from './listABTestsResponse'; export * from './minimumDetectableEffect'; diff --git a/clients/algoliasearch-client-javascript/packages/client-abtesting/model/minimumDetectableEffect.ts b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/minimumDetectableEffect.ts index 4b9bc0dfa1c..ecd87dcc1f5 100644 --- a/clients/algoliasearch-client-javascript/packages/client-abtesting/model/minimumDetectableEffect.ts +++ b/clients/algoliasearch-client-javascript/packages/client-abtesting/model/minimumDetectableEffect.ts @@ -1,6 +1,6 @@ // Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. -import type { Effect } from './effect'; +import type { EffectMetric } from './effectMetric'; /** * Configuration for the smallest difference between test variants you want to detect. @@ -9,7 +9,7 @@ export type MinimumDetectableEffect = { /** * Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. */ - size?: number; + size: number; - effect?: Effect; + metric: EffectMetric; }; diff --git a/clients/algoliasearch-client-javascript/packages/client-abtesting/src/abtestingClient.ts b/clients/algoliasearch-client-javascript/packages/client-abtesting/src/abtestingClient.ts index f85053920f1..cb08a55171a 100644 --- a/clients/algoliasearch-client-javascript/packages/client-abtesting/src/abtestingClient.ts +++ b/clients/algoliasearch-client-javascript/packages/client-abtesting/src/abtestingClient.ts @@ -14,6 +14,8 @@ import type { ABTest } from '../model/aBTest'; import type { ABTestResponse } from '../model/aBTestResponse'; import type { AddABTestsRequest } from '../model/addABTestsRequest'; +import type { EstimateABTestRequest } from '../model/estimateABTestRequest'; +import type { EstimateABTestResponse } from '../model/estimateABTestResponse'; import type { ListABTestsResponse } from '../model/listABTestsResponse'; import type { ScheduleABTestResponse } from '../model/scheduleABTestResponse'; import type { ScheduleABTestsRequest } from '../model/scheduleABTestsRequest'; @@ -298,6 +300,44 @@ export function createAbtestingClient({ return transporter.request(request, requestOptions); }, + /** + * Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + * + * Required API Key ACLs: + * - analytics + * @param estimateABTestRequest - The estimateABTestRequest object. + * @param requestOptions - The requestOptions to send along with the query, they will be merged with the transporter requestOptions. + */ + estimateABTest( + estimateABTestRequest: EstimateABTestRequest, + requestOptions?: RequestOptions, + ): Promise { + if (!estimateABTestRequest) { + throw new Error('Parameter `estimateABTestRequest` is required when calling `estimateABTest`.'); + } + + if (!estimateABTestRequest.configuration) { + throw new Error('Parameter `estimateABTestRequest.configuration` is required when calling `estimateABTest`.'); + } + if (!estimateABTestRequest.variants) { + throw new Error('Parameter `estimateABTestRequest.variants` is required when calling `estimateABTest`.'); + } + + const requestPath = '/2/abtests/estimate'; + const headers: Headers = {}; + const queryParameters: QueryParameters = {}; + + const request: Request = { + method: 'POST', + path: requestPath, + queryParameters, + headers, + data: estimateABTestRequest, + }; + + return transporter.request(request, requestOptions); + }, + /** * Retrieves the details for an A/B test by its ID. * diff --git a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/api/AbtestingClient.kt b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/api/AbtestingClient.kt index 4598df9a1c7..812057dc102 100644 --- a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/api/AbtestingClient.kt +++ b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/api/AbtestingClient.kt @@ -155,6 +155,26 @@ public class AbtestingClient( ) } + /** + * Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + * + * Required API Key ACLs: + * - analytics + * @param estimateABTestRequest + * @param requestOptions additional request configuration. + */ + public suspend fun estimateABTest(estimateABTestRequest: EstimateABTestRequest, requestOptions: RequestOptions? = null): EstimateABTestResponse { + val requestConfig = RequestConfig( + method = RequestMethod.POST, + path = listOf("2", "abtests", "estimate"), + body = estimateABTestRequest, + ) + return requester.execute( + requestConfig = requestConfig, + requestOptions = requestOptions, + ) + } + /** * Retrieves the details for an A/B test by its ID. * diff --git a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/Effect.kt b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EffectMetric.kt similarity index 91% rename from clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/Effect.kt rename to clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EffectMetric.kt index a73b9468590..bd3326e27d9 100644 --- a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/Effect.kt +++ b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EffectMetric.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.* * Metric for which you want to detect the smallest relative difference. */ @Serializable -public enum class Effect(public val value: kotlin.String) { +public enum class EffectMetric(public val value: kotlin.String) { @SerialName(value = "addToCartRate") AddToCartRate("addToCartRate"), diff --git a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EstimateABTestRequest.kt b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EstimateABTestRequest.kt new file mode 100644 index 00000000000..9e1a7d32837 --- /dev/null +++ b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EstimateABTestRequest.kt @@ -0,0 +1,20 @@ +/** Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. */ +package com.algolia.client.model.abtesting + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +/** + * EstimateABTestRequest + * + * @param configuration + * @param variants A/B test variants. + */ +@Serializable +public data class EstimateABTestRequest( + + @SerialName(value = "configuration") val configuration: EstimateConfiguration, + + /** A/B test variants. */ + @SerialName(value = "variants") val variants: List, +) diff --git a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EstimateABTestResponse.kt b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EstimateABTestResponse.kt new file mode 100644 index 00000000000..73c4e0c9037 --- /dev/null +++ b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EstimateABTestResponse.kt @@ -0,0 +1,25 @@ +/** Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. */ +package com.algolia.client.model.abtesting + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +/** + * EstimateABTestResponse + * + * @param durationDays Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value is based on historical traffic. + * @param controlSampleSize Number of tracked searches needed to be able to detect the configured effect for the control variant. + * @param experimentSampleSize Number of tracked searches needed to be able to detect the configured effect for the experiment variant. + */ +@Serializable +public data class EstimateABTestResponse( + + /** Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value is based on historical traffic. */ + @SerialName(value = "durationDays") val durationDays: Long? = null, + + /** Number of tracked searches needed to be able to detect the configured effect for the control variant. */ + @SerialName(value = "controlSampleSize") val controlSampleSize: Long? = null, + + /** Number of tracked searches needed to be able to detect the configured effect for the experiment variant. */ + @SerialName(value = "experimentSampleSize") val experimentSampleSize: Long? = null, +) diff --git a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EstimateConfiguration.kt b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EstimateConfiguration.kt new file mode 100644 index 00000000000..b6492afdcb0 --- /dev/null +++ b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/EstimateConfiguration.kt @@ -0,0 +1,22 @@ +/** Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. */ +package com.algolia.client.model.abtesting + +import kotlinx.serialization.* +import kotlinx.serialization.json.* + +/** + * A/B test configuration for estimating the sample size and duration using minimum detectable effect. + * + * @param minimumDetectableEffect + * @param outliers + * @param emptySearch + */ +@Serializable +public data class EstimateConfiguration( + + @SerialName(value = "minimumDetectableEffect") val minimumDetectableEffect: MinimumDetectableEffect, + + @SerialName(value = "outliers") val outliers: Outliers? = null, + + @SerialName(value = "emptySearch") val emptySearch: EmptySearch? = null, +) diff --git a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/MinimumDetectableEffect.kt b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/MinimumDetectableEffect.kt index d854da53409..4e5e3543dfa 100644 --- a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/MinimumDetectableEffect.kt +++ b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/abtesting/MinimumDetectableEffect.kt @@ -8,13 +8,13 @@ import kotlinx.serialization.json.* * Configuration for the smallest difference between test variants you want to detect. * * @param size Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. - * @param effect + * @param metric */ @Serializable public data class MinimumDetectableEffect( /** Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. */ - @SerialName(value = "size") val size: Double? = null, + @SerialName(value = "size") val size: Double, - @SerialName(value = "effect") val effect: Effect? = null, + @SerialName(value = "metric") val metric: EffectMetric, ) diff --git a/clients/algoliasearch-client-php/lib/Api/AbtestingClient.php b/clients/algoliasearch-client-php/lib/Api/AbtestingClient.php index 88a45839c5d..13e5d1128f1 100644 --- a/clients/algoliasearch-client-php/lib/Api/AbtestingClient.php +++ b/clients/algoliasearch-client-php/lib/Api/AbtestingClient.php @@ -7,6 +7,7 @@ use Algolia\AlgoliaSearch\Algolia; use Algolia\AlgoliaSearch\Configuration\AbtestingConfig; use Algolia\AlgoliaSearch\Model\Abtesting\AddABTestsRequest; +use Algolia\AlgoliaSearch\Model\Abtesting\EstimateABTestRequest; use Algolia\AlgoliaSearch\Model\Abtesting\ScheduleABTestsRequest; use Algolia\AlgoliaSearch\ObjectSerializer; use Algolia\AlgoliaSearch\RetryStrategy\ApiWrapper; @@ -338,6 +339,39 @@ public function deleteABTest($id, $requestOptions = []) return $this->sendRequest('DELETE', $resourcePath, $headers, $queryParameters, $httpBody, $requestOptions); } + /** + * Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + * + * Required API Key ACLs: + * - analytics + * + * @param array $estimateABTestRequest estimateABTestRequest (required) + * - $estimateABTestRequest['configuration'] => (array) (required) + * - $estimateABTestRequest['variants'] => (array) A/B test variants. (required) + * + * @see EstimateABTestRequest + * + * @param array $requestOptions the requestOptions to send along with the query, they will be merged with the transporter requestOptions + * + * @return \Algolia\AlgoliaSearch\Model\Abtesting\EstimateABTestResponse|array + */ + public function estimateABTest($estimateABTestRequest, $requestOptions = []) + { + // verify the required parameter 'estimateABTestRequest' is set + if (!isset($estimateABTestRequest)) { + throw new \InvalidArgumentException( + 'Parameter `estimateABTestRequest` is required when calling `estimateABTest`.' + ); + } + + $resourcePath = '/2/abtests/estimate'; + $queryParameters = []; + $headers = []; + $httpBody = $estimateABTestRequest; + + return $this->sendRequest('POST', $resourcePath, $headers, $queryParameters, $httpBody, $requestOptions); + } + /** * Retrieves the details for an A/B test by its ID. * diff --git a/clients/algoliasearch-client-php/lib/Model/Abtesting/Effect.php b/clients/algoliasearch-client-php/lib/Model/Abtesting/EffectMetric.php similarity index 94% rename from clients/algoliasearch-client-php/lib/Model/Abtesting/Effect.php rename to clients/algoliasearch-client-php/lib/Model/Abtesting/EffectMetric.php index ff96e387177..d171aaa8e3d 100644 --- a/clients/algoliasearch-client-php/lib/Model/Abtesting/Effect.php +++ b/clients/algoliasearch-client-php/lib/Model/Abtesting/EffectMetric.php @@ -5,13 +5,13 @@ namespace Algolia\AlgoliaSearch\Model\Abtesting; /** - * Effect Class Doc Comment. + * EffectMetric Class Doc Comment. * * @category Class * * @description Metric for which you want to detect the smallest relative difference. */ -class Effect +class EffectMetric { /** * Possible values of this enum. diff --git a/clients/algoliasearch-client-php/lib/Model/Abtesting/EstimateABTestRequest.php b/clients/algoliasearch-client-php/lib/Model/Abtesting/EstimateABTestRequest.php new file mode 100644 index 00000000000..f2746c17681 --- /dev/null +++ b/clients/algoliasearch-client-php/lib/Model/Abtesting/EstimateABTestRequest.php @@ -0,0 +1,265 @@ + '\Algolia\AlgoliaSearch\Model\Abtesting\EstimateConfiguration', + 'variants' => '\Algolia\AlgoliaSearch\Model\Abtesting\AddABTestsVariant[]', + ]; + + /** + * Array of property to format mappings. Used for (de)serialization. + * + * @var string[] + */ + protected static $modelFormats = [ + 'configuration' => null, + 'variants' => null, + ]; + + /** + * Array of attributes where the key is the local name, + * and the value is the original name. + * + * @var string[] + */ + protected static $attributeMap = [ + 'configuration' => 'configuration', + 'variants' => 'variants', + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses). + * + * @var string[] + */ + protected static $setters = [ + 'configuration' => 'setConfiguration', + 'variants' => 'setVariants', + ]; + + /** + * Array of attributes to getter functions (for serialization of requests). + * + * @var string[] + */ + protected static $getters = [ + 'configuration' => 'getConfiguration', + 'variants' => 'getVariants', + ]; + + /** + * Associative array for storing property values. + * + * @var mixed[] + */ + protected $container = []; + + /** + * Constructor. + * + * @param mixed[] $data Associated array of property values + */ + public function __construct(?array $data = null) + { + if (isset($data['configuration'])) { + $this->container['configuration'] = $data['configuration']; + } + if (isset($data['variants'])) { + $this->container['variants'] = $data['variants']; + } + } + + /** + * Array of attributes where the key is the local name, + * and the value is the original name. + * + * @return array + */ + public static function attributeMap() + { + return self::$attributeMap; + } + + /** + * Array of property to type mappings. Used for (de)serialization. + * + * @return array + */ + public static function modelTypes() + { + return self::$modelTypes; + } + + /** + * Array of property to format mappings. Used for (de)serialization. + * + * @return array + */ + public static function modelFormats() + { + return self::$modelFormats; + } + + /** + * Array of attributes to setter functions (for deserialization of responses). + * + * @return array + */ + public static function setters() + { + return self::$setters; + } + + /** + * Array of attributes to getter functions (for serialization of requests). + * + * @return array + */ + public static function getters() + { + return self::$getters; + } + + /** + * Show all the invalid properties with reasons. + * + * @return array invalid properties with reasons + */ + public function listInvalidProperties() + { + $invalidProperties = []; + + if (!isset($this->container['configuration']) || null === $this->container['configuration']) { + $invalidProperties[] = "'configuration' can't be null"; + } + if (!isset($this->container['variants']) || null === $this->container['variants']) { + $invalidProperties[] = "'variants' can't be null"; + } + + return $invalidProperties; + } + + /** + * Validate all the properties in the model + * return true if all passed. + * + * @return bool True if all properties are valid + */ + public function valid() + { + return 0 === count($this->listInvalidProperties()); + } + + /** + * Gets configuration. + * + * @return EstimateConfiguration + */ + public function getConfiguration() + { + return $this->container['configuration'] ?? null; + } + + /** + * Sets configuration. + * + * @param EstimateConfiguration $configuration configuration + * + * @return self + */ + public function setConfiguration($configuration) + { + $this->container['configuration'] = $configuration; + + return $this; + } + + /** + * Gets variants. + * + * @return \Algolia\AlgoliaSearch\Model\Abtesting\AddABTestsVariant[] + */ + public function getVariants() + { + return $this->container['variants'] ?? null; + } + + /** + * Sets variants. + * + * @param \Algolia\AlgoliaSearch\Model\Abtesting\AddABTestsVariant[] $variants A/B test variants + * + * @return self + */ + public function setVariants($variants) + { + $this->container['variants'] = $variants; + + return $this; + } + + /** + * Returns true if offset exists. False otherwise. + * + * @param int $offset Offset + */ + public function offsetExists($offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param int $offset Offset + * + * @return null|mixed + */ + public function offsetGet($offset): mixed + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param null|int $offset Offset + * @param mixed $value Value to be set + */ + public function offsetSet($offset, $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + /** + * Unsets offset. + * + * @param int $offset Offset + */ + public function offsetUnset($offset): void + { + unset($this->container[$offset]); + } +} diff --git a/clients/algoliasearch-client-php/lib/Model/Abtesting/EstimateABTestResponse.php b/clients/algoliasearch-client-php/lib/Model/Abtesting/EstimateABTestResponse.php new file mode 100644 index 00000000000..e67cd690eb5 --- /dev/null +++ b/clients/algoliasearch-client-php/lib/Model/Abtesting/EstimateABTestResponse.php @@ -0,0 +1,288 @@ + 'int', + 'controlSampleSize' => 'int', + 'experimentSampleSize' => 'int', + ]; + + /** + * Array of property to format mappings. Used for (de)serialization. + * + * @var string[] + */ + protected static $modelFormats = [ + 'durationDays' => 'int64', + 'controlSampleSize' => 'int64', + 'experimentSampleSize' => 'int64', + ]; + + /** + * Array of attributes where the key is the local name, + * and the value is the original name. + * + * @var string[] + */ + protected static $attributeMap = [ + 'durationDays' => 'durationDays', + 'controlSampleSize' => 'controlSampleSize', + 'experimentSampleSize' => 'experimentSampleSize', + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses). + * + * @var string[] + */ + protected static $setters = [ + 'durationDays' => 'setDurationDays', + 'controlSampleSize' => 'setControlSampleSize', + 'experimentSampleSize' => 'setExperimentSampleSize', + ]; + + /** + * Array of attributes to getter functions (for serialization of requests). + * + * @var string[] + */ + protected static $getters = [ + 'durationDays' => 'getDurationDays', + 'controlSampleSize' => 'getControlSampleSize', + 'experimentSampleSize' => 'getExperimentSampleSize', + ]; + + /** + * Associative array for storing property values. + * + * @var mixed[] + */ + protected $container = []; + + /** + * Constructor. + * + * @param mixed[] $data Associated array of property values + */ + public function __construct(?array $data = null) + { + if (isset($data['durationDays'])) { + $this->container['durationDays'] = $data['durationDays']; + } + if (isset($data['controlSampleSize'])) { + $this->container['controlSampleSize'] = $data['controlSampleSize']; + } + if (isset($data['experimentSampleSize'])) { + $this->container['experimentSampleSize'] = $data['experimentSampleSize']; + } + } + + /** + * Array of attributes where the key is the local name, + * and the value is the original name. + * + * @return array + */ + public static function attributeMap() + { + return self::$attributeMap; + } + + /** + * Array of property to type mappings. Used for (de)serialization. + * + * @return array + */ + public static function modelTypes() + { + return self::$modelTypes; + } + + /** + * Array of property to format mappings. Used for (de)serialization. + * + * @return array + */ + public static function modelFormats() + { + return self::$modelFormats; + } + + /** + * Array of attributes to setter functions (for deserialization of responses). + * + * @return array + */ + public static function setters() + { + return self::$setters; + } + + /** + * Array of attributes to getter functions (for serialization of requests). + * + * @return array + */ + public static function getters() + { + return self::$getters; + } + + /** + * Show all the invalid properties with reasons. + * + * @return array invalid properties with reasons + */ + public function listInvalidProperties() + { + return []; + } + + /** + * Validate all the properties in the model + * return true if all passed. + * + * @return bool True if all properties are valid + */ + public function valid() + { + return 0 === count($this->listInvalidProperties()); + } + + /** + * Gets durationDays. + * + * @return null|int + */ + public function getDurationDays() + { + return $this->container['durationDays'] ?? null; + } + + /** + * Sets durationDays. + * + * @param null|int $durationDays Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value is based on historical traffic. + * + * @return self + */ + public function setDurationDays($durationDays) + { + $this->container['durationDays'] = $durationDays; + + return $this; + } + + /** + * Gets controlSampleSize. + * + * @return null|int + */ + public function getControlSampleSize() + { + return $this->container['controlSampleSize'] ?? null; + } + + /** + * Sets controlSampleSize. + * + * @param null|int $controlSampleSize number of tracked searches needed to be able to detect the configured effect for the control variant + * + * @return self + */ + public function setControlSampleSize($controlSampleSize) + { + $this->container['controlSampleSize'] = $controlSampleSize; + + return $this; + } + + /** + * Gets experimentSampleSize. + * + * @return null|int + */ + public function getExperimentSampleSize() + { + return $this->container['experimentSampleSize'] ?? null; + } + + /** + * Sets experimentSampleSize. + * + * @param null|int $experimentSampleSize number of tracked searches needed to be able to detect the configured effect for the experiment variant + * + * @return self + */ + public function setExperimentSampleSize($experimentSampleSize) + { + $this->container['experimentSampleSize'] = $experimentSampleSize; + + return $this; + } + + /** + * Returns true if offset exists. False otherwise. + * + * @param int $offset Offset + */ + public function offsetExists($offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param int $offset Offset + * + * @return null|mixed + */ + public function offsetGet($offset): mixed + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param null|int $offset Offset + * @param mixed $value Value to be set + */ + public function offsetSet($offset, $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + /** + * Unsets offset. + * + * @param int $offset Offset + */ + public function offsetUnset($offset): void + { + unset($this->container[$offset]); + } +} diff --git a/clients/algoliasearch-client-php/lib/Model/Abtesting/EstimateConfiguration.php b/clients/algoliasearch-client-php/lib/Model/Abtesting/EstimateConfiguration.php new file mode 100644 index 00000000000..64105672641 --- /dev/null +++ b/clients/algoliasearch-client-php/lib/Model/Abtesting/EstimateConfiguration.php @@ -0,0 +1,296 @@ + '\Algolia\AlgoliaSearch\Model\Abtesting\Outliers', + 'emptySearch' => '\Algolia\AlgoliaSearch\Model\Abtesting\EmptySearch', + 'minimumDetectableEffect' => '\Algolia\AlgoliaSearch\Model\Abtesting\MinimumDetectableEffect', + ]; + + /** + * Array of property to format mappings. Used for (de)serialization. + * + * @var string[] + */ + protected static $modelFormats = [ + 'outliers' => null, + 'emptySearch' => null, + 'minimumDetectableEffect' => null, + ]; + + /** + * Array of attributes where the key is the local name, + * and the value is the original name. + * + * @var string[] + */ + protected static $attributeMap = [ + 'outliers' => 'outliers', + 'emptySearch' => 'emptySearch', + 'minimumDetectableEffect' => 'minimumDetectableEffect', + ]; + + /** + * Array of attributes to setter functions (for deserialization of responses). + * + * @var string[] + */ + protected static $setters = [ + 'outliers' => 'setOutliers', + 'emptySearch' => 'setEmptySearch', + 'minimumDetectableEffect' => 'setMinimumDetectableEffect', + ]; + + /** + * Array of attributes to getter functions (for serialization of requests). + * + * @var string[] + */ + protected static $getters = [ + 'outliers' => 'getOutliers', + 'emptySearch' => 'getEmptySearch', + 'minimumDetectableEffect' => 'getMinimumDetectableEffect', + ]; + + /** + * Associative array for storing property values. + * + * @var mixed[] + */ + protected $container = []; + + /** + * Constructor. + * + * @param mixed[] $data Associated array of property values + */ + public function __construct(?array $data = null) + { + if (isset($data['outliers'])) { + $this->container['outliers'] = $data['outliers']; + } + if (isset($data['emptySearch'])) { + $this->container['emptySearch'] = $data['emptySearch']; + } + if (isset($data['minimumDetectableEffect'])) { + $this->container['minimumDetectableEffect'] = $data['minimumDetectableEffect']; + } + } + + /** + * Array of attributes where the key is the local name, + * and the value is the original name. + * + * @return array + */ + public static function attributeMap() + { + return self::$attributeMap; + } + + /** + * Array of property to type mappings. Used for (de)serialization. + * + * @return array + */ + public static function modelTypes() + { + return self::$modelTypes; + } + + /** + * Array of property to format mappings. Used for (de)serialization. + * + * @return array + */ + public static function modelFormats() + { + return self::$modelFormats; + } + + /** + * Array of attributes to setter functions (for deserialization of responses). + * + * @return array + */ + public static function setters() + { + return self::$setters; + } + + /** + * Array of attributes to getter functions (for serialization of requests). + * + * @return array + */ + public static function getters() + { + return self::$getters; + } + + /** + * Show all the invalid properties with reasons. + * + * @return array invalid properties with reasons + */ + public function listInvalidProperties() + { + $invalidProperties = []; + + if (!isset($this->container['minimumDetectableEffect']) || null === $this->container['minimumDetectableEffect']) { + $invalidProperties[] = "'minimumDetectableEffect' can't be null"; + } + + return $invalidProperties; + } + + /** + * Validate all the properties in the model + * return true if all passed. + * + * @return bool True if all properties are valid + */ + public function valid() + { + return 0 === count($this->listInvalidProperties()); + } + + /** + * Gets outliers. + * + * @return null|Outliers + */ + public function getOutliers() + { + return $this->container['outliers'] ?? null; + } + + /** + * Sets outliers. + * + * @param null|Outliers $outliers outliers + * + * @return self + */ + public function setOutliers($outliers) + { + $this->container['outliers'] = $outliers; + + return $this; + } + + /** + * Gets emptySearch. + * + * @return null|EmptySearch + */ + public function getEmptySearch() + { + return $this->container['emptySearch'] ?? null; + } + + /** + * Sets emptySearch. + * + * @param null|EmptySearch $emptySearch emptySearch + * + * @return self + */ + public function setEmptySearch($emptySearch) + { + $this->container['emptySearch'] = $emptySearch; + + return $this; + } + + /** + * Gets minimumDetectableEffect. + * + * @return MinimumDetectableEffect + */ + public function getMinimumDetectableEffect() + { + return $this->container['minimumDetectableEffect'] ?? null; + } + + /** + * Sets minimumDetectableEffect. + * + * @param MinimumDetectableEffect $minimumDetectableEffect minimumDetectableEffect + * + * @return self + */ + public function setMinimumDetectableEffect($minimumDetectableEffect) + { + $this->container['minimumDetectableEffect'] = $minimumDetectableEffect; + + return $this; + } + + /** + * Returns true if offset exists. False otherwise. + * + * @param int $offset Offset + */ + public function offsetExists($offset): bool + { + return isset($this->container[$offset]); + } + + /** + * Gets offset. + * + * @param int $offset Offset + * + * @return null|mixed + */ + public function offsetGet($offset): mixed + { + return $this->container[$offset] ?? null; + } + + /** + * Sets value based on offset. + * + * @param null|int $offset Offset + * @param mixed $value Value to be set + */ + public function offsetSet($offset, $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + /** + * Unsets offset. + * + * @param int $offset Offset + */ + public function offsetUnset($offset): void + { + unset($this->container[$offset]); + } +} diff --git a/clients/algoliasearch-client-php/lib/Model/Abtesting/MinimumDetectableEffect.php b/clients/algoliasearch-client-php/lib/Model/Abtesting/MinimumDetectableEffect.php index 936652d4597..ae79f25a1bc 100644 --- a/clients/algoliasearch-client-php/lib/Model/Abtesting/MinimumDetectableEffect.php +++ b/clients/algoliasearch-client-php/lib/Model/Abtesting/MinimumDetectableEffect.php @@ -23,7 +23,7 @@ class MinimumDetectableEffect extends AbstractModel implements ModelInterface, \ */ protected static $modelTypes = [ 'size' => 'float', - 'effect' => '\Algolia\AlgoliaSearch\Model\Abtesting\Effect', + 'metric' => '\Algolia\AlgoliaSearch\Model\Abtesting\EffectMetric', ]; /** @@ -33,7 +33,7 @@ class MinimumDetectableEffect extends AbstractModel implements ModelInterface, \ */ protected static $modelFormats = [ 'size' => 'double', - 'effect' => null, + 'metric' => null, ]; /** @@ -44,7 +44,7 @@ class MinimumDetectableEffect extends AbstractModel implements ModelInterface, \ */ protected static $attributeMap = [ 'size' => 'size', - 'effect' => 'effect', + 'metric' => 'metric', ]; /** @@ -54,7 +54,7 @@ class MinimumDetectableEffect extends AbstractModel implements ModelInterface, \ */ protected static $setters = [ 'size' => 'setSize', - 'effect' => 'setEffect', + 'metric' => 'setMetric', ]; /** @@ -64,7 +64,7 @@ class MinimumDetectableEffect extends AbstractModel implements ModelInterface, \ */ protected static $getters = [ 'size' => 'getSize', - 'effect' => 'getEffect', + 'metric' => 'getMetric', ]; /** @@ -84,8 +84,8 @@ public function __construct(?array $data = null) if (isset($data['size'])) { $this->container['size'] = $data['size']; } - if (isset($data['effect'])) { - $this->container['effect'] = $data['effect']; + if (isset($data['metric'])) { + $this->container['metric'] = $data['metric']; } } @@ -147,7 +147,16 @@ public static function getters() */ public function listInvalidProperties() { - return []; + $invalidProperties = []; + + if (!isset($this->container['size']) || null === $this->container['size']) { + $invalidProperties[] = "'size' can't be null"; + } + if (!isset($this->container['metric']) || null === $this->container['metric']) { + $invalidProperties[] = "'metric' can't be null"; + } + + return $invalidProperties; } /** @@ -164,7 +173,7 @@ public function valid() /** * Gets size. * - * @return null|float + * @return float */ public function getSize() { @@ -174,7 +183,7 @@ public function getSize() /** * Sets size. * - * @param null|float $size Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. + * @param float $size Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. * * @return self */ @@ -186,25 +195,25 @@ public function setSize($size) } /** - * Gets effect. + * Gets metric. * - * @return null|Effect + * @return EffectMetric */ - public function getEffect() + public function getMetric() { - return $this->container['effect'] ?? null; + return $this->container['metric'] ?? null; } /** - * Sets effect. + * Sets metric. * - * @param null|Effect $effect effect + * @param EffectMetric $metric metric * * @return self */ - public function setEffect($effect) + public function setMetric($metric) { - $this->container['effect'] = $effect; + $this->container['metric'] = $metric; return $this; } diff --git a/clients/algoliasearch-client-python/algoliasearch/abtesting/client.py b/clients/algoliasearch-client-python/algoliasearch/abtesting/client.py index 680e521a5da..5ee554d9950 100644 --- a/clients/algoliasearch-client-python/algoliasearch/abtesting/client.py +++ b/clients/algoliasearch-client-python/algoliasearch/abtesting/client.py @@ -23,6 +23,12 @@ from algoliasearch.abtesting.models.ab_test import ABTest from algoliasearch.abtesting.models.ab_test_response import ABTestResponse from algoliasearch.abtesting.models.add_ab_tests_request import AddABTestsRequest +from algoliasearch.abtesting.models.estimate_ab_test_request import ( + EstimateABTestRequest, +) +from algoliasearch.abtesting.models.estimate_ab_test_response import ( + EstimateABTestResponse, +) from algoliasearch.abtesting.models.list_ab_tests_response import ListABTestsResponse from algoliasearch.abtesting.models.schedule_ab_test_response import ( ScheduleABTestResponse, @@ -567,6 +573,63 @@ async def delete_ab_test( resp = await self.delete_ab_test_with_http_info(id, request_options) return resp.deserialize(ABTestResponse, resp.raw_data) + async def estimate_ab_test_with_http_info( + self, + estimate_ab_test_request: Union[EstimateABTestRequest, dict[str, Any]], + request_options: Optional[Union[dict, RequestOptions]] = None, + ) -> ApiResponse[str]: + """ + Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + + Required API Key ACLs: + - analytics + + :param estimate_ab_test_request: (required) + :type estimate_ab_test_request: EstimateABTestRequest + :param request_options: The request options to send along with the query, they will be merged with the transporter base parameters (headers, query params, timeouts, etc.). (optional) + :return: Returns the raw algoliasearch 'APIResponse' object. + """ + + if estimate_ab_test_request is None: + raise ValueError( + "Parameter `estimate_ab_test_request` is required when calling `estimate_ab_test`." + ) + + _data = {} + if estimate_ab_test_request is not None: + _data = estimate_ab_test_request + + return await self._transporter.request( + verb=Verb.POST, + path="/2/abtests/estimate", + request_options=self._request_options.merge( + data=dumps(body_serializer(_data)), + user_request_options=request_options, + ), + use_read_transporter=False, + ) + + async def estimate_ab_test( + self, + estimate_ab_test_request: Union[EstimateABTestRequest, dict[str, Any]], + request_options: Optional[Union[dict, RequestOptions]] = None, + ) -> EstimateABTestResponse: + """ + Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + + Required API Key ACLs: + - analytics + + :param estimate_ab_test_request: (required) + :type estimate_ab_test_request: EstimateABTestRequest + :param request_options: The request options to send along with the query, they will be merged with the transporter base parameters (headers, query params, timeouts, etc.). (optional) + :return: Returns the deserialized response in a 'EstimateABTestResponse' result object. + """ + resp = await self.estimate_ab_test_with_http_info( + estimate_ab_test_request, request_options + ) + return resp.deserialize(EstimateABTestResponse, resp.raw_data) + async def get_ab_test_with_http_info( self, id: Annotated[StrictInt, Field(description="Unique A/B test identifier.")], @@ -1347,6 +1410,63 @@ def delete_ab_test( resp = self.delete_ab_test_with_http_info(id, request_options) return resp.deserialize(ABTestResponse, resp.raw_data) + def estimate_ab_test_with_http_info( + self, + estimate_ab_test_request: Union[EstimateABTestRequest, dict[str, Any]], + request_options: Optional[Union[dict, RequestOptions]] = None, + ) -> ApiResponse[str]: + """ + Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + + Required API Key ACLs: + - analytics + + :param estimate_ab_test_request: (required) + :type estimate_ab_test_request: EstimateABTestRequest + :param request_options: The request options to send along with the query, they will be merged with the transporter base parameters (headers, query params, timeouts, etc.). (optional) + :return: Returns the raw algoliasearch 'APIResponse' object. + """ + + if estimate_ab_test_request is None: + raise ValueError( + "Parameter `estimate_ab_test_request` is required when calling `estimate_ab_test`." + ) + + _data = {} + if estimate_ab_test_request is not None: + _data = estimate_ab_test_request + + return self._transporter.request( + verb=Verb.POST, + path="/2/abtests/estimate", + request_options=self._request_options.merge( + data=dumps(body_serializer(_data)), + user_request_options=request_options, + ), + use_read_transporter=False, + ) + + def estimate_ab_test( + self, + estimate_ab_test_request: Union[EstimateABTestRequest, dict[str, Any]], + request_options: Optional[Union[dict, RequestOptions]] = None, + ) -> EstimateABTestResponse: + """ + Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + + Required API Key ACLs: + - analytics + + :param estimate_ab_test_request: (required) + :type estimate_ab_test_request: EstimateABTestRequest + :param request_options: The request options to send along with the query, they will be merged with the transporter base parameters (headers, query params, timeouts, etc.). (optional) + :return: Returns the deserialized response in a 'EstimateABTestResponse' result object. + """ + resp = self.estimate_ab_test_with_http_info( + estimate_ab_test_request, request_options + ) + return resp.deserialize(EstimateABTestResponse, resp.raw_data) + def get_ab_test_with_http_info( self, id: Annotated[StrictInt, Field(description="Unique A/B test identifier.")], diff --git a/clients/algoliasearch-client-python/algoliasearch/abtesting/models/effect.py b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/effect_metric.py similarity index 89% rename from clients/algoliasearch-client-python/algoliasearch/abtesting/models/effect.py rename to clients/algoliasearch-client-python/algoliasearch/abtesting/models/effect_metric.py index a6569231c70..f4e512c92ef 100644 --- a/clients/algoliasearch-client-python/algoliasearch/abtesting/models/effect.py +++ b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/effect_metric.py @@ -16,7 +16,7 @@ from typing_extensions import Self -class Effect(str, Enum): +class EffectMetric(str, Enum): """ Metric for which you want to detect the smallest relative difference. """ @@ -34,5 +34,5 @@ class Effect(str, Enum): @classmethod def from_json(cls, json_str: str) -> Self: - """Create an instance of Effect from a JSON string""" + """Create an instance of EffectMetric from a JSON string""" return cls(loads(json_str)) diff --git a/clients/algoliasearch-client-python/algoliasearch/abtesting/models/estimate_ab_test_request.py b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/estimate_ab_test_request.py new file mode 100644 index 00000000000..fb5d2aa2700 --- /dev/null +++ b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/estimate_ab_test_request.py @@ -0,0 +1,87 @@ +# coding: utf-8 + +""" +Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +""" + +from __future__ import annotations + +from json import loads +from sys import version_info +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, ConfigDict + +if version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + + +from algoliasearch.abtesting.models.add_ab_tests_variant import AddABTestsVariant +from algoliasearch.abtesting.models.estimate_configuration import EstimateConfiguration + +_ALIASES = { + "configuration": "configuration", + "variants": "variants", +} + + +def _alias_generator(name: str) -> str: + return _ALIASES.get(name, name) + + +class EstimateABTestRequest(BaseModel): + """ + EstimateABTestRequest + """ + + configuration: EstimateConfiguration + variants: List[AddABTestsVariant] + """ A/B test variants. """ + + model_config = ConfigDict( + use_enum_values=True, + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + alias_generator=_alias_generator, + ) + + def to_json(self) -> str: + return self.model_dump_json(by_alias=True, exclude_unset=True) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of EstimateABTestRequest from a JSON string""" + return cls.from_dict(loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias.""" + return self.model_dump( + by_alias=True, + exclude_none=True, + exclude_unset=True, + ) + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of EstimateABTestRequest from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + obj["configuration"] = ( + EstimateConfiguration.from_dict(obj["configuration"]) + if obj.get("configuration") is not None + else None + ) + obj["variants"] = ( + [AddABTestsVariant.from_dict(_item) for _item in obj["variants"]] + if obj.get("variants") is not None + else None + ) + + return cls.model_validate(obj) diff --git a/clients/algoliasearch-client-python/algoliasearch/abtesting/models/estimate_ab_test_response.py b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/estimate_ab_test_response.py new file mode 100644 index 00000000000..010eebb7bf1 --- /dev/null +++ b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/estimate_ab_test_response.py @@ -0,0 +1,77 @@ +# coding: utf-8 + +""" +Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +""" + +from __future__ import annotations + +from json import loads +from sys import version_info +from typing import Any, Dict, Optional + +from pydantic import BaseModel, ConfigDict + +if version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + + +_ALIASES = { + "duration_days": "durationDays", + "control_sample_size": "controlSampleSize", + "experiment_sample_size": "experimentSampleSize", +} + + +def _alias_generator(name: str) -> str: + return _ALIASES.get(name, name) + + +class EstimateABTestResponse(BaseModel): + """ + EstimateABTestResponse + """ + + duration_days: Optional[int] = None + """ Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value is based on historical traffic. """ + control_sample_size: Optional[int] = None + """ Number of tracked searches needed to be able to detect the configured effect for the control variant. """ + experiment_sample_size: Optional[int] = None + """ Number of tracked searches needed to be able to detect the configured effect for the experiment variant. """ + + model_config = ConfigDict( + use_enum_values=True, + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + alias_generator=_alias_generator, + ) + + def to_json(self) -> str: + return self.model_dump_json(by_alias=True, exclude_unset=True) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of EstimateABTestResponse from a JSON string""" + return cls.from_dict(loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias.""" + return self.model_dump( + by_alias=True, + exclude_none=True, + exclude_unset=True, + ) + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of EstimateABTestResponse from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + return cls.model_validate(obj) diff --git a/clients/algoliasearch-client-python/algoliasearch/abtesting/models/estimate_configuration.py b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/estimate_configuration.py new file mode 100644 index 00000000000..7f7d1862424 --- /dev/null +++ b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/estimate_configuration.py @@ -0,0 +1,96 @@ +# coding: utf-8 + +""" +Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. +""" + +from __future__ import annotations + +from json import loads +from sys import version_info +from typing import Any, Dict, Optional + +from pydantic import BaseModel, ConfigDict + +if version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + + +from algoliasearch.abtesting.models.empty_search import EmptySearch +from algoliasearch.abtesting.models.minimum_detectable_effect import ( + MinimumDetectableEffect, +) +from algoliasearch.abtesting.models.outliers import Outliers + +_ALIASES = { + "outliers": "outliers", + "empty_search": "emptySearch", + "minimum_detectable_effect": "minimumDetectableEffect", +} + + +def _alias_generator(name: str) -> str: + return _ALIASES.get(name, name) + + +class EstimateConfiguration(BaseModel): + """ + A/B test configuration for estimating the sample size and duration using minimum detectable effect. + """ + + outliers: Optional[Outliers] = None + empty_search: Optional[EmptySearch] = None + minimum_detectable_effect: MinimumDetectableEffect + + model_config = ConfigDict( + use_enum_values=True, + populate_by_name=True, + validate_assignment=True, + protected_namespaces=(), + alias_generator=_alias_generator, + ) + + def to_json(self) -> str: + return self.model_dump_json(by_alias=True, exclude_unset=True) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of EstimateConfiguration from a JSON string""" + return cls.from_dict(loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias.""" + return self.model_dump( + by_alias=True, + exclude_none=True, + exclude_unset=True, + ) + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of EstimateConfiguration from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + obj["outliers"] = ( + Outliers.from_dict(obj["outliers"]) + if obj.get("outliers") is not None + else None + ) + obj["emptySearch"] = ( + EmptySearch.from_dict(obj["emptySearch"]) + if obj.get("emptySearch") is not None + else None + ) + obj["minimumDetectableEffect"] = ( + MinimumDetectableEffect.from_dict(obj["minimumDetectableEffect"]) + if obj.get("minimumDetectableEffect") is not None + else None + ) + + return cls.model_validate(obj) diff --git a/clients/algoliasearch-client-python/algoliasearch/abtesting/models/minimum_detectable_effect.py b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/minimum_detectable_effect.py index 57d8153565e..9e46fae0b3a 100644 --- a/clients/algoliasearch-client-python/algoliasearch/abtesting/models/minimum_detectable_effect.py +++ b/clients/algoliasearch-client-python/algoliasearch/abtesting/models/minimum_detectable_effect.py @@ -18,11 +18,11 @@ from typing_extensions import Self -from algoliasearch.abtesting.models.effect import Effect +from algoliasearch.abtesting.models.effect_metric import EffectMetric _ALIASES = { "size": "size", - "effect": "effect", + "metric": "metric", } @@ -35,9 +35,9 @@ class MinimumDetectableEffect(BaseModel): Configuration for the smallest difference between test variants you want to detect. """ - size: Optional[float] = None + size: float """ Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. """ - effect: Optional[Effect] = None + metric: EffectMetric model_config = ConfigDict( use_enum_values=True, @@ -72,6 +72,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - obj["effect"] = obj.get("effect") + obj["metric"] = obj.get("metric") return cls.model_validate(obj) diff --git a/clients/algoliasearch-client-ruby/lib/algolia/api/abtesting_client.rb b/clients/algoliasearch-client-ruby/lib/algolia/api/abtesting_client.rb index 5869a672706..61325f31cb8 100644 --- a/clients/algoliasearch-client-ruby/lib/algolia/api/abtesting_client.rb +++ b/clients/algoliasearch-client-ruby/lib/algolia/api/abtesting_client.rb @@ -319,6 +319,50 @@ def delete_ab_test(id, request_options = {}) @api_client.deserialize(response.body, request_options[:debug_return_type] || "Abtesting::ABTestResponse") end + # Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + # + # Required API Key ACLs: + # - analytics + # @param estimate_ab_test_request [EstimateABTestRequest] (required) + # @param request_options: The request options to send along with the query, they will be merged with the transporter base parameters (headers, query params, timeouts, etc.). (optional) + # @return [Http::Response] the response + def estimate_ab_test_with_http_info(estimate_ab_test_request, request_options = {}) + # verify the required parameter 'estimate_ab_test_request' is set + if @api_client.config.client_side_validation && estimate_ab_test_request.nil? + raise ArgumentError, "Parameter `estimate_ab_test_request` is required when calling `estimate_ab_test`." + end + + path = "/2/abtests/estimate" + query_params = {} + query_params = query_params.merge(request_options[:query_params]) unless request_options[:query_params].nil? + header_params = {} + header_params = header_params.merge(request_options[:header_params]) unless request_options[:header_params].nil? + + post_body = request_options[:debug_body] || @api_client.object_to_http_body(estimate_ab_test_request) + + new_options = request_options.merge( + :operation => :"AbtestingClient.estimate_ab_test", + :header_params => header_params, + :query_params => query_params, + :body => post_body, + :use_read_transporter => false + ) + + @api_client.call_api(:POST, path, new_options) + end + + # Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of an A/B test based on historical traffic. + # + # Required API Key ACLs: + # - analytics + # @param estimate_ab_test_request [EstimateABTestRequest] (required) + # @param request_options: The request options to send along with the query, they will be merged with the transporter base parameters (headers, query params, timeouts, etc.). (optional) + # @return [EstimateABTestResponse] + def estimate_ab_test(estimate_ab_test_request, request_options = {}) + response = estimate_ab_test_with_http_info(estimate_ab_test_request, request_options) + @api_client.deserialize(response.body, request_options[:debug_return_type] || "Abtesting::EstimateABTestResponse") + end + # Retrieves the details for an A/B test by its ID. # # Required API Key ACLs: diff --git a/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/effect.rb b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/effect_metric.rb similarity index 86% rename from clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/effect.rb rename to clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/effect_metric.rb index ea4df88eed7..6e8f6020be1 100644 --- a/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/effect.rb +++ b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/effect_metric.rb @@ -5,7 +5,7 @@ module Algolia module Abtesting - class Effect + class EffectMetric ADD_TO_CART_RATE = "addToCartRate".freeze CLICK_THROUGH_RATE = "clickThroughRate".freeze CONVERSION_RATE = "conversionRate".freeze @@ -26,8 +26,8 @@ def self.build_from_hash(value) # @param [String] The enum value in the form of the string # @return [String] The enum value def build_from_hash(value) - return value if Effect.all_vars.include?(value) - raise "Invalid ENUM value #{value} for class #Effect" + return value if EffectMetric.all_vars.include?(value) + raise "Invalid ENUM value #{value} for class #EffectMetric" end end end diff --git a/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/estimate_ab_test_request.rb b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/estimate_ab_test_request.rb new file mode 100644 index 00000000000..ecae98565e7 --- /dev/null +++ b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/estimate_ab_test_request.rb @@ -0,0 +1,227 @@ +# Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +require "date" +require "time" + +module Algolia + module Abtesting + class EstimateABTestRequest + attr_accessor :configuration + + # A/B test variants. + attr_accessor :variants + + # Attribute mapping from ruby-style variable name to JSON key. + def self.attribute_map + { + :configuration => :configuration, + :variants => :variants + } + end + + # Returns all the JSON keys this model knows about + def self.acceptable_attributes + attribute_map.values + end + + # Attribute type mapping. + def self.types_mapping + { + :configuration => :"EstimateConfiguration", + :variants => :"Array" + } + end + + # List of attributes with nullable: true + def self.openapi_nullable + Set.new( + [] + ) + end + + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize(attributes = {}) + if (!attributes.is_a?(Hash)) + raise( + ArgumentError, + "The input argument (attributes) must be a hash in `Algolia::EstimateABTestRequest` initialize method" + ) + end + + # check to see if the attribute exists and convert string to symbol for hash key + attributes = attributes.each_with_object({}) { |(k, v), h| + if (!self.class.attribute_map.key?(k.to_sym)) + raise( + ArgumentError, + "`#{k}` is not a valid attribute in `Algolia::EstimateABTestRequest`. Please check the name to make sure it's valid. List of attributes: " + + self.class.attribute_map.keys.inspect + ) + end + + h[k.to_sym] = v + } + + if attributes.key?(:configuration) + self.configuration = attributes[:configuration] + else + self.configuration = nil + end + + if attributes.key?(:variants) + if (value = attributes[:variants]).is_a?(Array) + self.variants = value + end + else + self.variants = nil + end + end + + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(other) + return true if self.equal?(other) + self.class == other.class && + configuration == other.configuration && + variants == other.variants + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(other) + self == other + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [configuration, variants].hash + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + attributes = attributes.transform_keys(&:to_sym) + transformed_hash = {} + types_mapping.each_pair do |key, type| + if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil? + transformed_hash[key.to_sym] = nil + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[attribute_map[key]].is_a?(Array) + transformed_hash[key.to_sym] = attributes[attribute_map[key]].map { |v| + _deserialize(::Regexp.last_match(1), v) + } + end + elsif !attributes[attribute_map[key]].nil? + transformed_hash[key.to_sym] = _deserialize(type, attributes[attribute_map[key]]) + end + end + + new(transformed_hash) + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def self._deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + {}.tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + # model + else + # models (e.g. Pet) or oneOf + klass = Algolia::Abtesting.const_get(type) + klass.respond_to?(:openapi_any_of) || klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass + .build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + def to_json(*_args) + to_hash.to_json + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {} + self.class.attribute_map.each_pair do |attr, param| + value = send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + {}.tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to?(:to_hash) + value.to_hash + else + value + end + end + + end + + end +end diff --git a/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/estimate_ab_test_response.rb b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/estimate_ab_test_response.rb new file mode 100644 index 00000000000..604ae8a1ff7 --- /dev/null +++ b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/estimate_ab_test_response.rb @@ -0,0 +1,232 @@ +# Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +require "date" +require "time" + +module Algolia + module Abtesting + class EstimateABTestResponse + # Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value is based on historical traffic. + attr_accessor :duration_days + + # Number of tracked searches needed to be able to detect the configured effect for the control variant. + attr_accessor :control_sample_size + + # Number of tracked searches needed to be able to detect the configured effect for the experiment variant. + attr_accessor :experiment_sample_size + + # Attribute mapping from ruby-style variable name to JSON key. + def self.attribute_map + { + :duration_days => :durationDays, + :control_sample_size => :controlSampleSize, + :experiment_sample_size => :experimentSampleSize + } + end + + # Returns all the JSON keys this model knows about + def self.acceptable_attributes + attribute_map.values + end + + # Attribute type mapping. + def self.types_mapping + { + :duration_days => :"Integer", + :control_sample_size => :"Integer", + :experiment_sample_size => :"Integer" + } + end + + # List of attributes with nullable: true + def self.openapi_nullable + Set.new( + [] + ) + end + + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize(attributes = {}) + if (!attributes.is_a?(Hash)) + raise( + ArgumentError, + "The input argument (attributes) must be a hash in `Algolia::EstimateABTestResponse` initialize method" + ) + end + + # check to see if the attribute exists and convert string to symbol for hash key + attributes = attributes.each_with_object({}) { |(k, v), h| + if (!self.class.attribute_map.key?(k.to_sym)) + raise( + ArgumentError, + "`#{k}` is not a valid attribute in `Algolia::EstimateABTestResponse`. Please check the name to make sure it's valid. List of attributes: " + + self.class.attribute_map.keys.inspect + ) + end + + h[k.to_sym] = v + } + + if attributes.key?(:duration_days) + self.duration_days = attributes[:duration_days] + end + + if attributes.key?(:control_sample_size) + self.control_sample_size = attributes[:control_sample_size] + end + + if attributes.key?(:experiment_sample_size) + self.experiment_sample_size = attributes[:experiment_sample_size] + end + end + + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(other) + return true if self.equal?(other) + self.class == other.class && + duration_days == other.duration_days && + control_sample_size == other.control_sample_size && + experiment_sample_size == other.experiment_sample_size + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(other) + self == other + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [duration_days, control_sample_size, experiment_sample_size].hash + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + attributes = attributes.transform_keys(&:to_sym) + transformed_hash = {} + types_mapping.each_pair do |key, type| + if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil? + transformed_hash[key.to_sym] = nil + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[attribute_map[key]].is_a?(Array) + transformed_hash[key.to_sym] = attributes[attribute_map[key]].map { |v| + _deserialize(::Regexp.last_match(1), v) + } + end + elsif !attributes[attribute_map[key]].nil? + transformed_hash[key.to_sym] = _deserialize(type, attributes[attribute_map[key]]) + end + end + + new(transformed_hash) + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def self._deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + {}.tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + # model + else + # models (e.g. Pet) or oneOf + klass = Algolia::Abtesting.const_get(type) + klass.respond_to?(:openapi_any_of) || klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass + .build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + def to_json(*_args) + to_hash.to_json + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {} + self.class.attribute_map.each_pair do |attr, param| + value = send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + {}.tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to?(:to_hash) + value.to_hash + else + value + end + end + + end + + end +end diff --git a/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/estimate_configuration.rb b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/estimate_configuration.rb new file mode 100644 index 00000000000..ec5e1e8719f --- /dev/null +++ b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/estimate_configuration.rb @@ -0,0 +1,232 @@ +# Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +require "date" +require "time" + +module Algolia + module Abtesting + # A/B test configuration for estimating the sample size and duration using minimum detectable effect. + class EstimateConfiguration + attr_accessor :outliers + + attr_accessor :empty_search + + attr_accessor :minimum_detectable_effect + + # Attribute mapping from ruby-style variable name to JSON key. + def self.attribute_map + { + :outliers => :outliers, + :empty_search => :emptySearch, + :minimum_detectable_effect => :minimumDetectableEffect + } + end + + # Returns all the JSON keys this model knows about + def self.acceptable_attributes + attribute_map.values + end + + # Attribute type mapping. + def self.types_mapping + { + :outliers => :"Outliers", + :empty_search => :"EmptySearch", + :minimum_detectable_effect => :"MinimumDetectableEffect" + } + end + + # List of attributes with nullable: true + def self.openapi_nullable + Set.new( + [] + ) + end + + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize(attributes = {}) + if (!attributes.is_a?(Hash)) + raise( + ArgumentError, + "The input argument (attributes) must be a hash in `Algolia::EstimateConfiguration` initialize method" + ) + end + + # check to see if the attribute exists and convert string to symbol for hash key + attributes = attributes.each_with_object({}) { |(k, v), h| + if (!self.class.attribute_map.key?(k.to_sym)) + raise( + ArgumentError, + "`#{k}` is not a valid attribute in `Algolia::EstimateConfiguration`. Please check the name to make sure it's valid. List of attributes: " + + self.class.attribute_map.keys.inspect + ) + end + + h[k.to_sym] = v + } + + if attributes.key?(:outliers) + self.outliers = attributes[:outliers] + end + + if attributes.key?(:empty_search) + self.empty_search = attributes[:empty_search] + end + + if attributes.key?(:minimum_detectable_effect) + self.minimum_detectable_effect = attributes[:minimum_detectable_effect] + else + self.minimum_detectable_effect = nil + end + end + + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(other) + return true if self.equal?(other) + self.class == other.class && + outliers == other.outliers && + empty_search == other.empty_search && + minimum_detectable_effect == other.minimum_detectable_effect + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(other) + self == other + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [outliers, empty_search, minimum_detectable_effect].hash + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + attributes = attributes.transform_keys(&:to_sym) + transformed_hash = {} + types_mapping.each_pair do |key, type| + if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil? + transformed_hash[key.to_sym] = nil + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[attribute_map[key]].is_a?(Array) + transformed_hash[key.to_sym] = attributes[attribute_map[key]].map { |v| + _deserialize(::Regexp.last_match(1), v) + } + end + elsif !attributes[attribute_map[key]].nil? + transformed_hash[key.to_sym] = _deserialize(type, attributes[attribute_map[key]]) + end + end + + new(transformed_hash) + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def self._deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + {}.tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + # model + else + # models (e.g. Pet) or oneOf + klass = Algolia::Abtesting.const_get(type) + klass.respond_to?(:openapi_any_of) || klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass + .build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + def to_json(*_args) + to_hash.to_json + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {} + self.class.attribute_map.each_pair do |attr, param| + value = send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + {}.tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to?(:to_hash) + value.to_hash + else + value + end + end + + end + + end +end diff --git a/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/minimum_detectable_effect.rb b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/minimum_detectable_effect.rb index 24b2eb9aed7..ffb93043385 100644 --- a/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/minimum_detectable_effect.rb +++ b/clients/algoliasearch-client-ruby/lib/algolia/models/abtesting/minimum_detectable_effect.rb @@ -10,13 +10,13 @@ class MinimumDetectableEffect # Smallest difference in an observable metric between variants. For example, to detect a 10% difference between variants, set this value to 0.1. attr_accessor :size - attr_accessor :effect + attr_accessor :metric # Attribute mapping from ruby-style variable name to JSON key. def self.attribute_map { :size => :size, - :effect => :effect + :metric => :metric } end @@ -29,7 +29,7 @@ def self.acceptable_attributes def self.types_mapping { :size => :"Float", - :effect => :"Effect" + :metric => :"EffectMetric" } end @@ -65,10 +65,14 @@ def initialize(attributes = {}) if attributes.key?(:size) self.size = attributes[:size] + else + self.size = nil end - if attributes.key?(:effect) - self.effect = attributes[:effect] + if attributes.key?(:metric) + self.metric = attributes[:metric] + else + self.metric = nil end end @@ -78,7 +82,7 @@ def ==(other) return true if self.equal?(other) self.class == other.class && size == other.size && - effect == other.effect + metric == other.metric end # @see the `==` method @@ -90,7 +94,7 @@ def eql?(other) # Calculates hash code according to all attributes. # @return [Integer] Hash code def hash - [size, effect].hash + [size, metric].hash end # Builds the object from hash diff --git a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/Effect.scala b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EffectMetric.scala similarity index 80% rename from clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/Effect.scala rename to clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EffectMetric.scala index d955999db66..ed01e15bb32 100644 --- a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/Effect.scala +++ b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EffectMetric.scala @@ -26,38 +26,38 @@ package algoliasearch.abtesting import org.json4s._ -sealed trait Effect +sealed trait EffectMetric /** Metric for which you want to detect the smallest relative difference. */ -object Effect { - case object AddToCartRate extends Effect { +object EffectMetric { + case object AddToCartRate extends EffectMetric { override def toString = "addToCartRate" } - case object ClickThroughRate extends Effect { + case object ClickThroughRate extends EffectMetric { override def toString = "clickThroughRate" } - case object ConversionRate extends Effect { + case object ConversionRate extends EffectMetric { override def toString = "conversionRate" } - case object PurchaseRate extends Effect { + case object PurchaseRate extends EffectMetric { override def toString = "purchaseRate" } - val values: Seq[Effect] = Seq(AddToCartRate, ClickThroughRate, ConversionRate, PurchaseRate) + val values: Seq[EffectMetric] = Seq(AddToCartRate, ClickThroughRate, ConversionRate, PurchaseRate) - def withName(name: String): Effect = Effect.values + def withName(name: String): EffectMetric = EffectMetric.values .find(_.toString == name) - .getOrElse(throw new MappingException(s"Unknown Effect value: $name")) + .getOrElse(throw new MappingException(s"Unknown EffectMetric value: $name")) } -class EffectSerializer - extends CustomSerializer[Effect](_ => +class EffectMetricSerializer + extends CustomSerializer[EffectMetric](_ => ( { - case JString(value) => Effect.withName(value) + case JString(value) => EffectMetric.withName(value) case JNull => null }, - { case value: Effect => + { case value: EffectMetric => JString(value.toString) } ) diff --git a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EstimateABTestRequest.scala b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EstimateABTestRequest.scala new file mode 100644 index 00000000000..9e8e12836a1 --- /dev/null +++ b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EstimateABTestRequest.scala @@ -0,0 +1,35 @@ +/** A/B Testing API The Algolia A/B Testing API lets you manage your Algolia A/B tests to optimize your search + * experience. ## Base URLs The base URLs for requests to the A/B testing API are: - `https://analytics.us.algolia.com` + * \- `https://analytics.de.algolia.com` - `https://analytics.algolia.com` (routes requests to the closest of the above + * servers, based on your geographical location) Use the URL that matches your [analytics + * region](https://dashboard.algolia.com/account/infrastructure/analytics). **All requests must use HTTPS.** ## + * Availability and authentication Access to the A/B testing API is available as part of the [Premium or Elevate + * plans](https://www.algolia.com/pricing). To authenticate your API requests, add these headers: - + * `x-algolia-application-id`. Your Algolia application ID. - `x-algolia-api-key`. An API key with the necessary + * permissions to make the request. The required access control list (ACL) to make a request is listed in each + * endpoint's reference. You can find your application ID and API key in the [Algolia + * dashboard](https://dashboard.algolia.com/account). ## Rate limits You can make up to **100 requests per minute per + * app** to the A/B testing API. The response includes headers with information about the limits. ## Parameters Query + * parameters must be [URL-encoded](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding). Non-ASCII + * characters must be UTF-8 encoded. Plus characters (`+`) are interpreted as spaces. ## Response status and errors The + * A/B testing API returns JSON responses. Since JSON doesn't guarantee any specific ordering, don't rely on the order + * of attributes in the API response. Successful responses return a `2xx` status. Client errors return a `4xx` status. + * Server errors are indicated by a `5xx` status. Error responses have a `message` property with more information. ## + * Version The current version of the A/B Testing API is version 2, as indicated by the `/2/` in each endpoint's URL. + * + * The version of the OpenAPI document: 2.0.0 + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech Do not edit the class manually. + */ +package algoliasearch.abtesting + +/** EstimateABTestRequest + * + * @param variants + * A/B test variants. + */ +case class EstimateABTestRequest( + configuration: EstimateConfiguration, + variants: Seq[AddABTestsVariant] +) diff --git a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EstimateABTestResponse.scala b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EstimateABTestResponse.scala new file mode 100644 index 00000000000..cdf2f32b672 --- /dev/null +++ b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EstimateABTestResponse.scala @@ -0,0 +1,41 @@ +/** A/B Testing API The Algolia A/B Testing API lets you manage your Algolia A/B tests to optimize your search + * experience. ## Base URLs The base URLs for requests to the A/B testing API are: - `https://analytics.us.algolia.com` + * \- `https://analytics.de.algolia.com` - `https://analytics.algolia.com` (routes requests to the closest of the above + * servers, based on your geographical location) Use the URL that matches your [analytics + * region](https://dashboard.algolia.com/account/infrastructure/analytics). **All requests must use HTTPS.** ## + * Availability and authentication Access to the A/B testing API is available as part of the [Premium or Elevate + * plans](https://www.algolia.com/pricing). To authenticate your API requests, add these headers: - + * `x-algolia-application-id`. Your Algolia application ID. - `x-algolia-api-key`. An API key with the necessary + * permissions to make the request. The required access control list (ACL) to make a request is listed in each + * endpoint's reference. You can find your application ID and API key in the [Algolia + * dashboard](https://dashboard.algolia.com/account). ## Rate limits You can make up to **100 requests per minute per + * app** to the A/B testing API. The response includes headers with information about the limits. ## Parameters Query + * parameters must be [URL-encoded](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding). Non-ASCII + * characters must be UTF-8 encoded. Plus characters (`+`) are interpreted as spaces. ## Response status and errors The + * A/B testing API returns JSON responses. Since JSON doesn't guarantee any specific ordering, don't rely on the order + * of attributes in the API response. Successful responses return a `2xx` status. Client errors return a `4xx` status. + * Server errors are indicated by a `5xx` status. Error responses have a `message` property with more information. ## + * Version The current version of the A/B Testing API is version 2, as indicated by the `/2/` in each endpoint's URL. + * + * The version of the OpenAPI document: 2.0.0 + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech Do not edit the class manually. + */ +package algoliasearch.abtesting + +/** EstimateABTestResponse + * + * @param durationDays + * Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This value + * is based on historical traffic. + * @param controlSampleSize + * Number of tracked searches needed to be able to detect the configured effect for the control variant. + * @param experimentSampleSize + * Number of tracked searches needed to be able to detect the configured effect for the experiment variant. + */ +case class EstimateABTestResponse( + durationDays: Option[Long] = scala.None, + controlSampleSize: Option[Long] = scala.None, + experimentSampleSize: Option[Long] = scala.None +) diff --git a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EstimateConfiguration.scala b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EstimateConfiguration.scala new file mode 100644 index 00000000000..b470fdcc650 --- /dev/null +++ b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/EstimateConfiguration.scala @@ -0,0 +1,33 @@ +/** A/B Testing API The Algolia A/B Testing API lets you manage your Algolia A/B tests to optimize your search + * experience. ## Base URLs The base URLs for requests to the A/B testing API are: - `https://analytics.us.algolia.com` + * \- `https://analytics.de.algolia.com` - `https://analytics.algolia.com` (routes requests to the closest of the above + * servers, based on your geographical location) Use the URL that matches your [analytics + * region](https://dashboard.algolia.com/account/infrastructure/analytics). **All requests must use HTTPS.** ## + * Availability and authentication Access to the A/B testing API is available as part of the [Premium or Elevate + * plans](https://www.algolia.com/pricing). To authenticate your API requests, add these headers: - + * `x-algolia-application-id`. Your Algolia application ID. - `x-algolia-api-key`. An API key with the necessary + * permissions to make the request. The required access control list (ACL) to make a request is listed in each + * endpoint's reference. You can find your application ID and API key in the [Algolia + * dashboard](https://dashboard.algolia.com/account). ## Rate limits You can make up to **100 requests per minute per + * app** to the A/B testing API. The response includes headers with information about the limits. ## Parameters Query + * parameters must be [URL-encoded](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding). Non-ASCII + * characters must be UTF-8 encoded. Plus characters (`+`) are interpreted as spaces. ## Response status and errors The + * A/B testing API returns JSON responses. Since JSON doesn't guarantee any specific ordering, don't rely on the order + * of attributes in the API response. Successful responses return a `2xx` status. Client errors return a `4xx` status. + * Server errors are indicated by a `5xx` status. Error responses have a `message` property with more information. ## + * Version The current version of the A/B Testing API is version 2, as indicated by the `/2/` in each endpoint's URL. + * + * The version of the OpenAPI document: 2.0.0 + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech Do not edit the class manually. + */ +package algoliasearch.abtesting + +/** A/B test configuration for estimating the sample size and duration using minimum detectable effect. + */ +case class EstimateConfiguration( + outliers: Option[Outliers] = scala.None, + emptySearch: Option[EmptySearch] = scala.None, + minimumDetectableEffect: MinimumDetectableEffect +) diff --git a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/JsonSupport.scala b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/JsonSupport.scala index 682e4e557d4..216fcebff49 100644 --- a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/JsonSupport.scala +++ b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/JsonSupport.scala @@ -28,7 +28,7 @@ import org.json4s._ object JsonSupport { private def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ - new EffectSerializer() :+ + new EffectMetricSerializer() :+ new StatusSerializer() private def oneOfsSerializers: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ diff --git a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/MinimumDetectableEffect.scala b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/MinimumDetectableEffect.scala index 67cb7fcd25f..8101906ff5c 100644 --- a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/MinimumDetectableEffect.scala +++ b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/abtesting/MinimumDetectableEffect.scala @@ -24,7 +24,7 @@ */ package algoliasearch.abtesting -import algoliasearch.abtesting.Effect._ +import algoliasearch.abtesting.EffectMetric._ /** Configuration for the smallest difference between test variants you want to detect. * @@ -33,6 +33,6 @@ import algoliasearch.abtesting.Effect._ * variants, set this value to 0.1. */ case class MinimumDetectableEffect( - size: Option[Double] = scala.None, - effect: Option[Effect] = scala.None + size: Double, + metric: EffectMetric ) diff --git a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/api/AbtestingClient.scala b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/api/AbtestingClient.scala index 8928dd3928d..ff5fcace339 100644 --- a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/api/AbtestingClient.scala +++ b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/api/AbtestingClient.scala @@ -7,6 +7,8 @@ import algoliasearch.abtesting.ABTest import algoliasearch.abtesting.ABTestResponse import algoliasearch.abtesting.AddABTestsRequest import algoliasearch.abtesting.ErrorBase +import algoliasearch.abtesting.EstimateABTestRequest +import algoliasearch.abtesting.EstimateABTestResponse import algoliasearch.abtesting.ListABTestsResponse import algoliasearch.abtesting.ScheduleABTestResponse import algoliasearch.abtesting.ScheduleABTestsRequest @@ -209,6 +211,29 @@ class AbtestingClient( execute[ABTestResponse](request, requestOptions) } + /** Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration of + * an A/B test based on historical traffic. + * + * Required API Key ACLs: + * - analytics + */ + def estimateABTest(estimateABTestRequest: EstimateABTestRequest, requestOptions: Option[RequestOptions] = None)( + implicit ec: ExecutionContext + ): Future[EstimateABTestResponse] = Future { + requireNotNull( + estimateABTestRequest, + "Parameter `estimateABTestRequest` is required when calling `estimateABTest`." + ) + + val request = HttpRequest + .builder() + .withMethod("POST") + .withPath(s"/2/abtests/estimate") + .withBody(estimateABTestRequest) + .build() + execute[EstimateABTestResponse](request, requestOptions) + } + /** Retrieves the details for an A/B test by its ID. * * Required API Key ACLs: diff --git a/clients/algoliasearch-client-swift/Sources/Abtesting/AbtestingClient.swift b/clients/algoliasearch-client-swift/Sources/Abtesting/AbtestingClient.swift index 74bc5c8ac7e..7cc494fd9db 100644 --- a/clients/algoliasearch-client-swift/Sources/Abtesting/AbtestingClient.swift +++ b/clients/algoliasearch-client-swift/Sources/Abtesting/AbtestingClient.swift @@ -384,6 +384,53 @@ open class AbtestingClient { ) } + /// - parameter estimateABTestRequest: (body) + /// - returns: EstimateABTestResponse + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) + open func estimateABTest( + estimateABTestRequest: EstimateABTestRequest, + requestOptions: RequestOptions? = nil + ) async throws -> EstimateABTestResponse { + let response: Response = try await estimateABTestWithHTTPInfo( + estimateABTestRequest: estimateABTestRequest, + requestOptions: requestOptions + ) + + guard let body = response.body else { + throw AlgoliaError.missingData + } + + return body + } + + // Given the traffic percentage and the expected effect size, this endpoint estimates the sample size and duration + // of an A/B test based on historical traffic. + // Required API Key ACLs: + // - analytics + // + // - parameter estimateABTestRequest: (body) + // - returns: RequestBuilder + + open func estimateABTestWithHTTPInfo( + estimateABTestRequest: EstimateABTestRequest, + requestOptions userRequestOptions: RequestOptions? = nil + ) async throws -> Response { + let resourcePath = "/2/abtests/estimate" + let body = estimateABTestRequest + let queryParameters: [String: Any?]? = nil + + let nillableHeaders: [String: Any?]? = nil + + let headers = APIHelper.rejectNilHeaders(nillableHeaders) + + return try await self.transporter.send( + method: "POST", + path: resourcePath, + data: body, + requestOptions: RequestOptions(headers: headers, queryParameters: queryParameters) + userRequestOptions + ) + } + /// - parameter id: (path) Unique A/B test identifier. /// - returns: ABTest @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) diff --git a/clients/algoliasearch-client-swift/Sources/Abtesting/Models/Effect.swift b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EffectMetric.swift similarity index 81% rename from clients/algoliasearch-client-swift/Sources/Abtesting/Models/Effect.swift rename to clients/algoliasearch-client-swift/Sources/Abtesting/Models/EffectMetric.swift index 81f337a6fb7..57ec40fbd4a 100644 --- a/clients/algoliasearch-client-swift/Sources/Abtesting/Models/Effect.swift +++ b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EffectMetric.swift @@ -7,11 +7,11 @@ import Foundation #endif /// Metric for which you want to detect the smallest relative difference. -public enum Effect: String, Codable, CaseIterable { +public enum EffectMetric: String, Codable, CaseIterable { case addToCartRate case clickThroughRate case conversionRate case purchaseRate } -extension Effect: Hashable {} +extension EffectMetric: Hashable {} diff --git a/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EstimateABTestRequest.swift b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EstimateABTestRequest.swift new file mode 100644 index 00000000000..83d8809081d --- /dev/null +++ b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EstimateABTestRequest.swift @@ -0,0 +1,45 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on +// https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +import Foundation +#if canImport(Core) + import Core +#endif + +public struct EstimateABTestRequest: Codable, JSONEncodable { + public var configuration: EstimateConfiguration + /// A/B test variants. + public var variants: [AddABTestsVariant] + + public init(configuration: EstimateConfiguration, variants: [AddABTestsVariant]) { + self.configuration = configuration + self.variants = variants + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case configuration + case variants + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.configuration, forKey: .configuration) + try container.encode(self.variants, forKey: .variants) + } +} + +extension EstimateABTestRequest: Equatable { + public static func ==(lhs: EstimateABTestRequest, rhs: EstimateABTestRequest) -> Bool { + lhs.configuration == rhs.configuration && + lhs.variants == rhs.variants + } +} + +extension EstimateABTestRequest: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.configuration.hashValue) + hasher.combine(self.variants.hashValue) + } +} diff --git a/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EstimateABTestResponse.swift b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EstimateABTestResponse.swift new file mode 100644 index 00000000000..fc8dee5eab1 --- /dev/null +++ b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EstimateABTestResponse.swift @@ -0,0 +1,54 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on +// https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +import Foundation +#if canImport(Core) + import Core +#endif + +public struct EstimateABTestResponse: Codable, JSONEncodable { + /// Estimated number of days needed to reach the sample sizes required for detecting the configured effect. This + /// value is based on historical traffic. + public var durationDays: Int64? + /// Number of tracked searches needed to be able to detect the configured effect for the control variant. + public var controlSampleSize: Int64? + /// Number of tracked searches needed to be able to detect the configured effect for the experiment variant. + public var experimentSampleSize: Int64? + + public init(durationDays: Int64? = nil, controlSampleSize: Int64? = nil, experimentSampleSize: Int64? = nil) { + self.durationDays = durationDays + self.controlSampleSize = controlSampleSize + self.experimentSampleSize = experimentSampleSize + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case durationDays + case controlSampleSize + case experimentSampleSize + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.durationDays, forKey: .durationDays) + try container.encodeIfPresent(self.controlSampleSize, forKey: .controlSampleSize) + try container.encodeIfPresent(self.experimentSampleSize, forKey: .experimentSampleSize) + } +} + +extension EstimateABTestResponse: Equatable { + public static func ==(lhs: EstimateABTestResponse, rhs: EstimateABTestResponse) -> Bool { + lhs.durationDays == rhs.durationDays && + lhs.controlSampleSize == rhs.controlSampleSize && + lhs.experimentSampleSize == rhs.experimentSampleSize + } +} + +extension EstimateABTestResponse: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.durationDays?.hashValue) + hasher.combine(self.controlSampleSize?.hashValue) + hasher.combine(self.experimentSampleSize?.hashValue) + } +} diff --git a/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EstimateConfiguration.swift b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EstimateConfiguration.swift new file mode 100644 index 00000000000..555ebb99fd1 --- /dev/null +++ b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/EstimateConfiguration.swift @@ -0,0 +1,55 @@ +// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on +// https://github.com/algolia/api-clients-automation. DO NOT EDIT. + +import Foundation +#if canImport(Core) + import Core +#endif + +/// A/B test configuration for estimating the sample size and duration using minimum detectable effect. +public struct EstimateConfiguration: Codable, JSONEncodable { + public var outliers: Outliers? + public var emptySearch: EmptySearch? + public var minimumDetectableEffect: MinimumDetectableEffect + + public init( + outliers: Outliers? = nil, + emptySearch: EmptySearch? = nil, + minimumDetectableEffect: MinimumDetectableEffect + ) { + self.outliers = outliers + self.emptySearch = emptySearch + self.minimumDetectableEffect = minimumDetectableEffect + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case outliers + case emptySearch + case minimumDetectableEffect + } + + // Encodable protocol methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.outliers, forKey: .outliers) + try container.encodeIfPresent(self.emptySearch, forKey: .emptySearch) + try container.encode(self.minimumDetectableEffect, forKey: .minimumDetectableEffect) + } +} + +extension EstimateConfiguration: Equatable { + public static func ==(lhs: EstimateConfiguration, rhs: EstimateConfiguration) -> Bool { + lhs.outliers == rhs.outliers && + lhs.emptySearch == rhs.emptySearch && + lhs.minimumDetectableEffect == rhs.minimumDetectableEffect + } +} + +extension EstimateConfiguration: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.outliers?.hashValue) + hasher.combine(self.emptySearch?.hashValue) + hasher.combine(self.minimumDetectableEffect.hashValue) + } +} diff --git a/clients/algoliasearch-client-swift/Sources/Abtesting/Models/MinimumDetectableEffect.swift b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/MinimumDetectableEffect.swift index e4621ff9727..bb4a276cd31 100644 --- a/clients/algoliasearch-client-swift/Sources/Abtesting/Models/MinimumDetectableEffect.swift +++ b/clients/algoliasearch-client-swift/Sources/Abtesting/Models/MinimumDetectableEffect.swift @@ -10,38 +10,38 @@ import Foundation public struct MinimumDetectableEffect: Codable, JSONEncodable { /// Smallest difference in an observable metric between variants. For example, to detect a 10% difference between /// variants, set this value to 0.1. - public var size: Double? - public var effect: Effect? + public var size: Double + public var metric: EffectMetric - public init(size: Double? = nil, effect: Effect? = nil) { + public init(size: Double, metric: EffectMetric) { self.size = size - self.effect = effect + self.metric = metric } public enum CodingKeys: String, CodingKey, CaseIterable { case size - case effect + case metric } // Encodable protocol methods public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(self.size, forKey: .size) - try container.encodeIfPresent(self.effect, forKey: .effect) + try container.encode(self.size, forKey: .size) + try container.encode(self.metric, forKey: .metric) } } extension MinimumDetectableEffect: Equatable { public static func ==(lhs: MinimumDetectableEffect, rhs: MinimumDetectableEffect) -> Bool { lhs.size == rhs.size && - lhs.effect == rhs.effect + lhs.metric == rhs.metric } } extension MinimumDetectableEffect: Hashable { public func hash(into hasher: inout Hasher) { - hasher.combine(self.size?.hashValue) - hasher.combine(self.effect?.hashValue) + hasher.combine(self.size.hashValue) + hasher.combine(self.metric.hashValue) } } diff --git a/docs/bundled/abtesting-snippets.json b/docs/bundled/abtesting-snippets.json index 4ca2b8e07f3..55ac7bdf924 100644 --- a/docs/bundled/abtesting-snippets.json +++ b/docs/bundled/abtesting-snippets.json @@ -21,6 +21,9 @@ "deleteABTest": { "default": "var response = await client.DeleteABTestAsync(42);" }, + "estimateABTest": { + "default": "var response = await client.EstimateABTestAsync(\n new EstimateABTestRequest\n {\n Configuration = new EstimateConfiguration\n {\n EmptySearch = new EmptySearch { Exclude = true },\n MinimumDetectableEffect = new MinimumDetectableEffect\n {\n Size = 0.03,\n Metric = Enum.Parse(\"ConversionRate\"),\n },\n },\n Variants = new List\n {\n new AddABTestsVariant(new AbTestsVariant { Index = \"AB_TEST_1\", TrafficPercentage = 50 }),\n new AddABTestsVariant(new AbTestsVariant { Index = \"AB_TEST_2\", TrafficPercentage = 50 }),\n },\n }\n);" + }, "getABTest": { "default": "var response = await client.GetABTestAsync(42);" }, @@ -63,6 +66,9 @@ "deleteABTest": { "default": "response, err := client.DeleteABTest(client.NewApiDeleteABTestRequest(\n 42,\n))\nif err != nil {\n // handle the eventual error\n panic(err)\n}" }, + "estimateABTest": { + "default": "response, err := client.EstimateABTest(client.NewApiEstimateABTestRequest(\n\n abtesting.NewEmptyEstimateABTestRequest().SetConfiguration(\n abtesting.NewEmptyEstimateConfiguration().SetEmptySearch(\n abtesting.NewEmptyEmptySearch().SetExclude(true)).SetMinimumDetectableEffect(\n abtesting.NewEmptyMinimumDetectableEffect().SetSize(0.03).SetMetric(abtesting.EffectMetric(\"conversionRate\")))).SetVariants(\n []abtesting.AddABTestsVariant{*abtesting.AbTestsVariantAsAddABTestsVariant(\n abtesting.NewEmptyAbTestsVariant().SetIndex(\"AB_TEST_1\").SetTrafficPercentage(50)), *abtesting.AbTestsVariantAsAddABTestsVariant(\n abtesting.NewEmptyAbTestsVariant().SetIndex(\"AB_TEST_2\").SetTrafficPercentage(50))}),\n))\nif err != nil {\n // handle the eventual error\n panic(err)\n}" + }, "getABTest": { "default": "response, err := client.GetABTest(client.NewApiGetABTestRequest(\n 42,\n))\nif err != nil {\n // handle the eventual error\n panic(err)\n}" }, @@ -104,6 +110,9 @@ "deleteABTest": { "default": "client.deleteABTest(42);" }, + "estimateABTest": { + "default": "client.estimateABTest(\n new EstimateABTestRequest()\n .setConfiguration(\n new EstimateConfiguration()\n .setEmptySearch(new EmptySearch().setExclude(true))\n .setMinimumDetectableEffect(new MinimumDetectableEffect().setSize(0.03).setMetric(EffectMetric.CONVERSION_RATE))\n )\n .setVariants(\n Arrays.asList(\n new AbTestsVariant().setIndex(\"AB_TEST_1\").setTrafficPercentage(50),\n new AbTestsVariant().setIndex(\"AB_TEST_2\").setTrafficPercentage(50)\n )\n )\n);" + }, "getABTest": { "default": "client.getABTest(42);" }, @@ -145,6 +154,9 @@ "deleteABTest": { "default": "const response = await client.deleteABTest({ id: 42 });" }, + "estimateABTest": { + "default": "const response = await client.estimateABTest({\n configuration: {\n emptySearch: { exclude: true },\n minimumDetectableEffect: { size: 0.03, metric: 'conversionRate' },\n },\n variants: [\n { index: 'AB_TEST_1', trafficPercentage: 50 },\n { index: 'AB_TEST_2', trafficPercentage: 50 },\n ],\n});" + }, "getABTest": { "default": "const response = await client.getABTest({ id: 42 });" }, @@ -186,6 +198,9 @@ "deleteABTest": { "default": "var response = client.deleteABTest(\n id = 42,\n)" }, + "estimateABTest": { + "default": "var response = client.estimateABTest(\n estimateABTestRequest = EstimateABTestRequest(\n configuration = EstimateConfiguration(\n emptySearch = EmptySearch(\n exclude = true,\n ),\n minimumDetectableEffect = MinimumDetectableEffect(\n size = 0.03,\n metric = EffectMetric.entries.first { it.value == \"conversionRate\" },\n ),\n ),\n variants = listOf(\n AbTestsVariant(\n index = \"AB_TEST_1\",\n trafficPercentage = 50,\n ),\n AbTestsVariant(\n index = \"AB_TEST_2\",\n trafficPercentage = 50,\n ),\n ),\n ),\n)" + }, "getABTest": { "default": "var response = client.getABTest(\n id = 42,\n)" }, @@ -227,6 +242,9 @@ "deleteABTest": { "default": "$response = $client->deleteABTest(\n 42,\n);" }, + "estimateABTest": { + "default": "$response = $client->estimateABTest(\n ['configuration' => ['emptySearch' => ['exclude' => true,\n ],\n 'minimumDetectableEffect' => ['size' => 0.03,\n 'metric' => 'conversionRate',\n ],\n ],\n 'variants' => [\n ['index' => 'AB_TEST_1',\n 'trafficPercentage' => 50,\n ],\n\n ['index' => 'AB_TEST_2',\n 'trafficPercentage' => 50,\n ],\n ],\n ],\n);" + }, "getABTest": { "default": "$response = $client->getABTest(\n 42,\n);" }, @@ -268,6 +286,9 @@ "deleteABTest": { "default": "response = client.delete_ab_test(\n id=42,\n)" }, + "estimateABTest": { + "default": "response = client.estimate_ab_test(\n estimate_ab_test_request={\n \"configuration\": {\n \"emptySearch\": {\n \"exclude\": True,\n },\n \"minimumDetectableEffect\": {\n \"size\": 0.03,\n \"metric\": \"conversionRate\",\n },\n },\n \"variants\": [\n {\n \"index\": \"AB_TEST_1\",\n \"trafficPercentage\": 50,\n },\n {\n \"index\": \"AB_TEST_2\",\n \"trafficPercentage\": 50,\n },\n ],\n },\n)" + }, "getABTest": { "default": "response = client.get_ab_test(\n id=42,\n)" }, @@ -309,6 +330,9 @@ "deleteABTest": { "default": "response = client.delete_ab_test(42)" }, + "estimateABTest": { + "default": "response = client.estimate_ab_test(\n Algolia::Abtesting::EstimateABTestRequest.new(\n configuration: Algolia::Abtesting::EstimateConfiguration.new(\n empty_search: Algolia::Abtesting::EmptySearch.new(exclude: true),\n minimum_detectable_effect: Algolia::Abtesting::MinimumDetectableEffect.new(size: 0.03, metric: \"conversionRate\")\n ),\n variants: [\n Algolia::Abtesting::AbTestsVariant.new(index: \"AB_TEST_1\", traffic_percentage: 50),\n Algolia::Abtesting::AbTestsVariant.new(index: \"AB_TEST_2\", traffic_percentage: 50)\n ]\n )\n)" + }, "getABTest": { "default": "response = client.get_ab_test(42)" }, @@ -350,6 +374,9 @@ "deleteABTest": { "default": "val response = client.deleteABTest(\n id = 42\n)" }, + "estimateABTest": { + "default": "val response = client.estimateABTest(\n estimateABTestRequest = EstimateABTestRequest(\n configuration = EstimateConfiguration(\n emptySearch = Some(\n EmptySearch(\n exclude = Some(true)\n )\n ),\n minimumDetectableEffect = MinimumDetectableEffect(\n size = 0.03,\n metric = EffectMetric.withName(\"conversionRate\")\n )\n ),\n variants = Seq(\n AbTestsVariant(\n index = \"AB_TEST_1\",\n trafficPercentage = 50\n ),\n AbTestsVariant(\n index = \"AB_TEST_2\",\n trafficPercentage = 50\n )\n )\n )\n)" + }, "getABTest": { "default": "val response = client.getABTest(\n id = 42\n)" }, @@ -388,6 +415,9 @@ "deleteABTest": { "default": "let response = try await client.deleteABTest(id: 42)" }, + "estimateABTest": { + "default": "let response = try await client.estimateABTest(estimateABTestRequest: EstimateABTestRequest(\n configuration: EstimateConfiguration(\n emptySearch: EmptySearch(exclude: true),\n minimumDetectableEffect: MinimumDetectableEffect(\n size: 0.03,\n metric: EffectMetric.conversionRate\n )\n ),\n variants: [\n AddABTestsVariant.abTestsVariant(AbTestsVariant(index: \"AB_TEST_1\", trafficPercentage: 50)),\n AddABTestsVariant.abTestsVariant(AbTestsVariant(index: \"AB_TEST_2\", trafficPercentage: 50)),\n ]\n))" + }, "getABTest": { "default": "let response = try await client.getABTest(id: 42)" }, diff --git a/docs/snippets/csharp/src/Abtesting.cs b/docs/snippets/csharp/src/Abtesting.cs index dbc1fa705f0..6dcf683d9d7 100644 --- a/docs/snippets/csharp/src/Abtesting.cs +++ b/docs/snippets/csharp/src/Abtesting.cs @@ -132,6 +132,43 @@ public async Task SnippetForAbtestingClientDeleteABTest() // SEPARATOR< } + /// + /// Snippet for the EstimateABTest method. + /// + /// estimate AB Test sample size + /// + public async Task SnippetForAbtestingClientEstimateABTest() + { + // >SEPARATOR estimateABTest default + // Initialize the client + var client = new AbtestingClient( + new AbtestingConfig("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY", "ALGOLIA_APPLICATION_REGION") + ); + + // Call the API + var response = await client.EstimateABTestAsync( + new EstimateABTestRequest + { + Configuration = new EstimateConfiguration + { + EmptySearch = new EmptySearch { Exclude = true }, + MinimumDetectableEffect = new MinimumDetectableEffect + { + Size = 0.03, + Metric = Enum.Parse("ConversionRate"), + }, + }, + Variants = new List + { + new AddABTestsVariant(new AbTestsVariant { Index = "AB_TEST_1", TrafficPercentage = 50 }), + new AddABTestsVariant(new AbTestsVariant { Index = "AB_TEST_2", TrafficPercentage = 50 }), + }, + } + ); + // >LOG + // SEPARATOR< + } + /// /// Snippet for the GetABTest method. /// diff --git a/docs/snippets/go/src/abtesting.go b/docs/snippets/go/src/abtesting.go index ad2d837bd38..8fda5adeaec 100644 --- a/docs/snippets/go/src/abtesting.go +++ b/docs/snippets/go/src/abtesting.go @@ -184,6 +184,42 @@ func SnippetForDeleteABTestOfAbtesting() { print(response) // SEPARATOR< } +func SnippetForEstimateABTestOfAbtesting() { + /* + Snippet for the estimateABTest method. + + estimate AB Test sample size + */ + + // >SEPARATOR estimateABTest default + // Initialize the client with your application region, eg. abtesting.ALGOLIA_APPLICATION_REGION + client, err := abtesting.NewClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY", abtesting.US) + if err != nil { + // The client can fail to initialize if you pass an invalid parameter. + panic(err) + } + + // Call the API + response, err := client.EstimateABTest(client.NewApiEstimateABTestRequest( + + abtesting.NewEmptyEstimateABTestRequest().SetConfiguration( + abtesting.NewEmptyEstimateConfiguration().SetEmptySearch( + abtesting.NewEmptyEmptySearch().SetExclude(true)).SetMinimumDetectableEffect( + abtesting.NewEmptyMinimumDetectableEffect().SetSize(0.03).SetMetric(abtesting.EffectMetric("conversionRate")))).SetVariants( + []abtesting.AddABTestsVariant{*abtesting.AbTestsVariantAsAddABTestsVariant( + abtesting.NewEmptyAbTestsVariant().SetIndex("AB_TEST_1").SetTrafficPercentage(50)), *abtesting.AbTestsVariantAsAddABTestsVariant( + abtesting.NewEmptyAbTestsVariant().SetIndex("AB_TEST_2").SetTrafficPercentage(50))}), + )) + if err != nil { + // handle the eventual error + panic(err) + } + + // >LOG + // use the model directly + print(response) + // SEPARATOR< +} func SnippetForGetABTestOfAbtesting() { /* Snippet for the getABTest method. diff --git a/docs/snippets/java/src/test/java/com/algolia/Abtesting.java b/docs/snippets/java/src/test/java/com/algolia/Abtesting.java index 8b40160da11..5c80ceddc1d 100644 --- a/docs/snippets/java/src/test/java/com/algolia/Abtesting.java +++ b/docs/snippets/java/src/test/java/com/algolia/Abtesting.java @@ -102,6 +102,33 @@ void snippetForDeleteABTest() throws Exception { // SEPARATOR< } + // Snippet for the estimateABTest method. + // + // estimate AB Test sample size + void snippetForEstimateABTest() throws Exception { + // >SEPARATOR estimateABTest default + // Initialize the client + AbtestingClient client = new AbtestingClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY", "ALGOLIA_APPLICATION_REGION"); + + // Call the API + client.estimateABTest( + new EstimateABTestRequest() + .setConfiguration( + new EstimateConfiguration() + .setEmptySearch(new EmptySearch().setExclude(true)) + .setMinimumDetectableEffect(new MinimumDetectableEffect().setSize(0.03).setMetric(EffectMetric.CONVERSION_RATE)) + ) + .setVariants( + Arrays.asList( + new AbTestsVariant().setIndex("AB_TEST_1").setTrafficPercentage(50), + new AbTestsVariant().setIndex("AB_TEST_2").setTrafficPercentage(50) + ) + ) + ); + // >LOG + // SEPARATOR< + } + // Snippet for the getABTest method. // // getABTest diff --git a/docs/snippets/javascript/src/abtesting.ts b/docs/snippets/javascript/src/abtesting.ts index 54badc78d66..e13a1c659e0 100644 --- a/docs/snippets/javascript/src/abtesting.ts +++ b/docs/snippets/javascript/src/abtesting.ts @@ -119,6 +119,33 @@ export async function snippetForDeleteABTest(): Promise { // SEPARATOR< } +// Snippet for the estimateABTest method. +// +// estimate AB Test sample size +export async function snippetForEstimateABTest(): Promise { + // >SEPARATOR estimateABTest default + // Initialize the client + // Replace 'us' with your Algolia Application Region + const client = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_API_KEY').initAbtesting({ region: 'us' }); + + // Call the API + const response = await client.estimateABTest({ + configuration: { + emptySearch: { exclude: true }, + minimumDetectableEffect: { size: 0.03, metric: 'conversionRate' }, + }, + variants: [ + { index: 'AB_TEST_1', trafficPercentage: 50 }, + { index: 'AB_TEST_2', trafficPercentage: 50 }, + ], + }); + + // >LOG + // use typed response + console.log(response); + // SEPARATOR< +} + // Snippet for the getABTest method. // // getABTest diff --git a/docs/snippets/kotlin/src/main/kotlin/com/algolia/snippets/Abtesting.kt b/docs/snippets/kotlin/src/main/kotlin/com/algolia/snippets/Abtesting.kt index 21c6f47cd27..2ad3719a772 100644 --- a/docs/snippets/kotlin/src/main/kotlin/com/algolia/snippets/Abtesting.kt +++ b/docs/snippets/kotlin/src/main/kotlin/com/algolia/snippets/Abtesting.kt @@ -131,6 +131,44 @@ class SnippetAbtestingClient { exitProcess(0) } + suspend fun snippetForEstimateABTest() { + // >SEPARATOR estimateABTest default + // Initialize the client + val client = AbtestingClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY", region = "ALGOLIA_APPLICATION_REGION") + + // Call the API + var response = client.estimateABTest( + estimateABTestRequest = EstimateABTestRequest( + configuration = EstimateConfiguration( + emptySearch = EmptySearch( + exclude = true, + ), + minimumDetectableEffect = MinimumDetectableEffect( + size = 0.03, + metric = EffectMetric.entries.first { it.value == "conversionRate" }, + ), + ), + variants = listOf( + AbTestsVariant( + index = "AB_TEST_1", + trafficPercentage = 50, + ), + AbTestsVariant( + index = "AB_TEST_2", + trafficPercentage = 50, + ), + ), + ), + ) + + // >LOG + // Use the response + println(response) + // SEPARATOR< + + exitProcess(0) + } + suspend fun snippetForGetABTest() { // >SEPARATOR getABTest default // Initialize the client diff --git a/docs/snippets/php/src/Abtesting.php b/docs/snippets/php/src/Abtesting.php index 7fd526d7a06..9575c35fc8f 100644 --- a/docs/snippets/php/src/Abtesting.php +++ b/docs/snippets/php/src/Abtesting.php @@ -154,6 +154,43 @@ public function snippetForDeleteABTest(): void // SEPARATOR< } + /** + * Snippet for the EstimateABTest method. + * + * estimate AB Test sample size + */ + public function snippetForEstimateABTest(): void + { + // >SEPARATOR estimateABTest default + // Initialize the client + $client = AbtestingClient::create('ALGOLIA_APPLICATION_ID', 'ALGOLIA_API_KEY', 'ALGOLIA_APPLICATION_REGION'); + + // Call the API + $response = $client->estimateABTest( + ['configuration' => ['emptySearch' => ['exclude' => true, + ], + 'minimumDetectableEffect' => ['size' => 0.03, + 'metric' => 'conversionRate', + ], + ], + 'variants' => [ + ['index' => 'AB_TEST_1', + 'trafficPercentage' => 50, + ], + + ['index' => 'AB_TEST_2', + 'trafficPercentage' => 50, + ], + ], + ], + ); + + // >LOG + // play with the response + var_dump($response); + // SEPARATOR< + } + /** * Snippet for the GetABTest method. * diff --git a/docs/snippets/python/abtesting.py b/docs/snippets/python/abtesting.py index 7bdae35cb11..69d17f8292e 100644 --- a/docs/snippets/python/abtesting.py +++ b/docs/snippets/python/abtesting.py @@ -161,6 +161,50 @@ def snippet_for_delete_ab_test(): # SEPARATOR< +def snippet_for_estimate_ab_test(): + """ + Snippet for the estimateABTest method. + + estimate AB Test sample size + """ + # >SEPARATOR estimateABTest default + # Initialize the client + # In an asynchronous context, you can use AbtestingClient instead, which exposes the exact same methods. + client = AbtestingClientSync( + "ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY", "ALGOLIA_APPLICATION_REGION" + ) + + # Call the API + response = client.estimate_ab_test( + estimate_ab_test_request={ + "configuration": { + "emptySearch": { + "exclude": True, + }, + "minimumDetectableEffect": { + "size": 0.03, + "metric": "conversionRate", + }, + }, + "variants": [ + { + "index": "AB_TEST_1", + "trafficPercentage": 50, + }, + { + "index": "AB_TEST_2", + "trafficPercentage": 50, + }, + ], + }, + ) + + # >LOG + # use the class directly + print(response) + # SEPARATOR< + + def snippet_for_get_ab_test(): """ Snippet for the getABTest method. diff --git a/docs/snippets/ruby/abtesting.rb b/docs/snippets/ruby/abtesting.rb index 1ae685ff6a6..a7d1aaba8a4 100644 --- a/docs/snippets/ruby/abtesting.rb +++ b/docs/snippets/ruby/abtesting.rb @@ -133,6 +133,37 @@ def snippet_for_delete_ab_test # SEPARATOR< end +# Snippet for the estimateABTest method. +# +# estimate AB Test sample size +def snippet_for_estimate_ab_test + # >SEPARATOR estimateABTest default + # Initialize the client + client = Algolia::AbtestingClient.create("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY", "ALGOLIA_APPLICATION_REGION") + + # Call the API + response = client.estimate_ab_test( + Algolia::Abtesting::EstimateABTestRequest.new( + configuration: Algolia::Abtesting::EstimateConfiguration.new( + empty_search: Algolia::Abtesting::EmptySearch.new(exclude: true), + minimum_detectable_effect: Algolia::Abtesting::MinimumDetectableEffect.new(size: 0.03, metric: "conversionRate") + ), + variants: [ + Algolia::Abtesting::AbTestsVariant.new(index: "AB_TEST_1", traffic_percentage: 50), + Algolia::Abtesting::AbTestsVariant.new(index: "AB_TEST_2", traffic_percentage: 50) + ] + ) + ) + + # >LOG + # use the class directly + puts(response) + + # print the JSON response + puts(response.to_json) + # SEPARATOR< +end + # Snippet for the getABTest method. # # getABTest diff --git a/docs/snippets/scala/src/main/scala/Abtesting.scala b/docs/snippets/scala/src/main/scala/Abtesting.scala index 974a6afc334..b8199c1ebeb 100644 --- a/docs/snippets/scala/src/main/scala/Abtesting.scala +++ b/docs/snippets/scala/src/main/scala/Abtesting.scala @@ -174,6 +174,52 @@ class SnippetAbtestingClient { // SEPARATOR< } + /** Snippet for the estimateABTest method. + * + * estimate AB Test sample size + */ + def snippetForAbtestingClientEstimateABTest(): Unit = { + // >SEPARATOR estimateABTest default + // Initialize the client + val client = AbtestingClient( + appId = "ALGOLIA_APPLICATION_ID", + apiKey = "ALGOLIA_API_KEY", + region = Option("ALGOLIA_APPLICATION_REGION") + ) + + // Call the API + val response = client.estimateABTest( + estimateABTestRequest = EstimateABTestRequest( + configuration = EstimateConfiguration( + emptySearch = Some( + EmptySearch( + exclude = Some(true) + ) + ), + minimumDetectableEffect = MinimumDetectableEffect( + size = 0.03, + metric = EffectMetric.withName("conversionRate") + ) + ), + variants = Seq( + AbTestsVariant( + index = "AB_TEST_1", + trafficPercentage = 50 + ), + AbTestsVariant( + index = "AB_TEST_2", + trafficPercentage = 50 + ) + ) + ) + ) + + // >LOG + // Use the response + val value = Await.result(response, Duration(100, "sec")) + // SEPARATOR< + } + /** Snippet for the getABTest method. * * getABTest diff --git a/docs/snippets/swift/Sources/Abtesting.swift b/docs/snippets/swift/Sources/Abtesting.swift index 8cf7a777274..ba197b8b743 100644 --- a/docs/snippets/swift/Sources/Abtesting.swift +++ b/docs/snippets/swift/Sources/Abtesting.swift @@ -98,6 +98,32 @@ final class AbtestingClientSnippet { // SEPARATOR< } + /// Snippet for the estimateABTest method. + /// + /// estimate AB Test sample size + func snippetForEstimateABTest() async throws { + // >SEPARATOR estimateABTest default + // Initialize the client + let client = try AbtestingClient(appID: "ALGOLIA_APPLICATION_ID", apiKey: "ALGOLIA_API_KEY", region: .us) + + // Call the API + let response = try await client.estimateABTest(estimateABTestRequest: EstimateABTestRequest( + configuration: EstimateConfiguration( + emptySearch: EmptySearch(exclude: true), + minimumDetectableEffect: MinimumDetectableEffect( + size: 0.03, + metric: EffectMetric.conversionRate + ) + ), + variants: [ + AddABTestsVariant.abTestsVariant(AbTestsVariant(index: "AB_TEST_1", trafficPercentage: 50)), + AddABTestsVariant.abTestsVariant(AbTestsVariant(index: "AB_TEST_2", trafficPercentage: 50)), + ] + )) + // >LOG + // SEPARATOR< + } + /// Snippet for the getABTest method. /// /// getABTest diff --git a/specs/bundled/abtesting.doc.yml b/specs/bundled/abtesting.doc.yml index 587679d8ac8..910da09027a 100644 --- a/specs/bundled/abtesting.doc.yml +++ b/specs/bundled/abtesting.doc.yml @@ -107,8 +107,8 @@ tags: Manage A/B tests. - A/B tests are configurations of two indices, usually your production index - and an index with different settings that you want to test. + A/B tests are configurations one or more indices, usually your production + index and an index with different settings that you want to test. externalDocs: url: https://www.algolia.com/doc/guides/ab-testing/what-is-ab-testing/ description: | @@ -2569,6 +2569,436 @@ paths: --header 'x-algolia-api-key: ALGOLIA_API_KEY' \ --header 'x-algolia-application-id: ALGOLIA_APPLICATION_ID' \ --data '{"name":"Custom ranking sales rank test","variants":[{"index":"delcourt_production","trafficPercentage":60,"description":"Current production index"},{"index":"delcourt_production","trafficPercentage":60,"description":"Current production index"}],"scheduledAt":"2023-06-15T15:06:44.400601Z","endAt":"2023-06-17T00:00:00Z"}' + /2/abtests/estimate: + post: + tags: + - abtest + operationId: estimateABTest + x-acl: + - analytics + summary: Estimate the sample size and duration of an A/B test + description: >- + Given the traffic percentage and the expected effect size, this endpoint + estimates the sample size and duration of an A/B test based on + historical traffic. + requestBody: + required: true + content: + application/json: + schema: + title: estimateABTestRequest + type: object + additionalProperties: false + properties: + configuration: + title: estimateConfiguration + type: object + description: >- + A/B test configuration for estimating the sample size and + duration using minimum detectable effect. + properties: + outliers: + $ref: '#/components/schemas/Outliers' + emptySearch: + $ref: '#/components/schemas/EmptySearch' + minimumDetectableEffect: + $ref: '#/components/schemas/MinimumDetectableEffect' + required: + - minimumDetectableEffect + variants: + type: array + description: A/B test variants. + minItems: 2 + maxItems: 2 + items: + $ref: '#/components/schemas/AddABTestsVariant' + required: + - configuration + - variants + responses: + '200': + description: OK + headers: + x-ratelimit-limit: + $ref: '#/components/headers/x-ratelimit-limit' + x-ratelimit-remaining: + $ref: '#/components/headers/x-ratelimit-remaining' + x-ratelimit-reset: + $ref: '#/components/headers/x-ratelimit-reset' + content: + application/json: + schema: + $ref: '#/components/schemas/EstimateABTestResponse' + '400': + $ref: '#/components/responses/BadRequest' + '402': + $ref: '#/components/responses/FeatureNotEnabled' + '403': + $ref: '#/components/responses/MethodNotAllowed' + '404': + $ref: '#/components/responses/IndexNotFound' + x-codeSamples: + - lang: csharp + label: C# + source: | + // Initialize the client + var client = new AbtestingClient( + new AbtestingConfig("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY", "ALGOLIA_APPLICATION_REGION") + ); + + // Call the API + var response = await client.EstimateABTestAsync( + new EstimateABTestRequest + { + Configuration = new EstimateConfiguration + { + EmptySearch = new EmptySearch { Exclude = true }, + MinimumDetectableEffect = new MinimumDetectableEffect + { + Size = 0.03, + Metric = Enum.Parse("ConversionRate"), + }, + }, + Variants = new List + { + new AddABTestsVariant(new AbTestsVariant { Index = "AB_TEST_1", TrafficPercentage = 50 }), + new AddABTestsVariant(new AbTestsVariant { Index = "AB_TEST_2", TrafficPercentage = 50 }), + }, + } + ); + // >LOG + - lang: go + label: Go + source: > + // Initialize the client with your application region, eg. + abtesting.ALGOLIA_APPLICATION_REGION + + client, err := abtesting.NewClient("ALGOLIA_APPLICATION_ID", + "ALGOLIA_API_KEY", abtesting.US) + + if err != nil { + // The client can fail to initialize if you pass an invalid parameter. + panic(err) + } + + + // Call the API + + response, err := + client.EstimateABTest(client.NewApiEstimateABTestRequest( + + abtesting.NewEmptyEstimateABTestRequest().SetConfiguration( + abtesting.NewEmptyEstimateConfiguration().SetEmptySearch( + abtesting.NewEmptyEmptySearch().SetExclude(true)).SetMinimumDetectableEffect( + abtesting.NewEmptyMinimumDetectableEffect().SetSize(0.03).SetMetric(abtesting.EffectMetric("conversionRate")))).SetVariants( + []abtesting.AddABTestsVariant{*abtesting.AbTestsVariantAsAddABTestsVariant( + abtesting.NewEmptyAbTestsVariant().SetIndex("AB_TEST_1").SetTrafficPercentage(50)), *abtesting.AbTestsVariantAsAddABTestsVariant( + abtesting.NewEmptyAbTestsVariant().SetIndex("AB_TEST_2").SetTrafficPercentage(50))}), + )) + + if err != nil { + // handle the eventual error + panic(err) + } + + + // >LOG + + // use the model directly + + print(response) + - lang: java + label: Java + source: > + // Initialize the client + + AbtestingClient client = new + AbtestingClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY", + "ALGOLIA_APPLICATION_REGION"); + + + // Call the API + + client.estimateABTest( + new EstimateABTestRequest() + .setConfiguration( + new EstimateConfiguration() + .setEmptySearch(new EmptySearch().setExclude(true)) + .setMinimumDetectableEffect(new MinimumDetectableEffect().setSize(0.03).setMetric(EffectMetric.CONVERSION_RATE)) + ) + .setVariants( + Arrays.asList( + new AbTestsVariant().setIndex("AB_TEST_1").setTrafficPercentage(50), + new AbTestsVariant().setIndex("AB_TEST_2").setTrafficPercentage(50) + ) + ) + ); + + // >LOG + - lang: javascript + label: JavaScript + source: > + // Initialize the client + + // Replace 'us' with your Algolia Application Region + + const client = algoliasearch('ALGOLIA_APPLICATION_ID', + 'ALGOLIA_API_KEY').initAbtesting({ region: 'us' }); + + + // Call the API + + const response = await client.estimateABTest({ + configuration: { + emptySearch: { exclude: true }, + minimumDetectableEffect: { size: 0.03, metric: 'conversionRate' }, + }, + variants: [ + { index: 'AB_TEST_1', trafficPercentage: 50 }, + { index: 'AB_TEST_2', trafficPercentage: 50 }, + ], + }); + + + // >LOG + + // use typed response + + console.log(response); + - lang: kotlin + label: Kotlin + source: > + // Initialize the client + + val client = AbtestingClient(appId = "ALGOLIA_APPLICATION_ID", + apiKey = "ALGOLIA_API_KEY", region = "ALGOLIA_APPLICATION_REGION") + + + // Call the API + + var response = client.estimateABTest( + estimateABTestRequest = EstimateABTestRequest( + configuration = EstimateConfiguration( + emptySearch = EmptySearch( + exclude = true, + ), + minimumDetectableEffect = MinimumDetectableEffect( + size = 0.03, + metric = EffectMetric.entries.first { it.value == "conversionRate" }, + ), + ), + variants = listOf( + AbTestsVariant( + index = "AB_TEST_1", + trafficPercentage = 50, + ), + AbTestsVariant( + index = "AB_TEST_2", + trafficPercentage = 50, + ), + ), + ), + ) + + + // >LOG + + // Use the response + + println(response) + - lang: php + label: PHP + source: > + // Initialize the client + + $client = AbtestingClient::create('ALGOLIA_APPLICATION_ID', + 'ALGOLIA_API_KEY', 'ALGOLIA_APPLICATION_REGION'); + + + // Call the API + + $response = $client->estimateABTest( + ['configuration' => ['emptySearch' => ['exclude' => true, + ], + 'minimumDetectableEffect' => ['size' => 0.03, + 'metric' => 'conversionRate', + ], + ], + 'variants' => [ + ['index' => 'AB_TEST_1', + 'trafficPercentage' => 50, + ], + + ['index' => 'AB_TEST_2', + 'trafficPercentage' => 50, + ], + ], + ], + ); + + + // >LOG + + // play with the response + + var_dump($response); + - lang: python + label: Python + source: > + # Initialize the client + + # In an asynchronous context, you can use AbtestingClient instead, + which exposes the exact same methods. + + client = AbtestingClientSync( + "ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY", "ALGOLIA_APPLICATION_REGION" + ) + + + # Call the API + + response = client.estimate_ab_test( + estimate_ab_test_request={ + "configuration": { + "emptySearch": { + "exclude": True, + }, + "minimumDetectableEffect": { + "size": 0.03, + "metric": "conversionRate", + }, + }, + "variants": [ + { + "index": "AB_TEST_1", + "trafficPercentage": 50, + }, + { + "index": "AB_TEST_2", + "trafficPercentage": 50, + }, + ], + }, + ) + + + # >LOG + + # use the class directly + + print(response) + - lang: ruby + label: Ruby + source: > + # Initialize the client + + client = Algolia::AbtestingClient.create("ALGOLIA_APPLICATION_ID", + "ALGOLIA_API_KEY", "ALGOLIA_APPLICATION_REGION") + + + # Call the API + + response = client.estimate_ab_test( + Algolia::Abtesting::EstimateABTestRequest.new( + configuration: Algolia::Abtesting::EstimateConfiguration.new( + empty_search: Algolia::Abtesting::EmptySearch.new(exclude: true), + minimum_detectable_effect: Algolia::Abtesting::MinimumDetectableEffect.new(size: 0.03, metric: "conversionRate") + ), + variants: [ + Algolia::Abtesting::AbTestsVariant.new(index: "AB_TEST_1", traffic_percentage: 50), + Algolia::Abtesting::AbTestsVariant.new(index: "AB_TEST_2", traffic_percentage: 50) + ] + ) + ) + + + # >LOG + + # use the class directly + + puts(response) + + + # print the JSON response + + puts(response.to_json) + - lang: scala + label: Scala + source: | + // Initialize the client + val client = AbtestingClient( + appId = "ALGOLIA_APPLICATION_ID", + apiKey = "ALGOLIA_API_KEY", + region = Option("ALGOLIA_APPLICATION_REGION") + ) + + // Call the API + val response = client.estimateABTest( + estimateABTestRequest = EstimateABTestRequest( + configuration = EstimateConfiguration( + emptySearch = Some( + EmptySearch( + exclude = Some(true) + ) + ), + minimumDetectableEffect = MinimumDetectableEffect( + size = 0.03, + metric = EffectMetric.withName("conversionRate") + ) + ), + variants = Seq( + AbTestsVariant( + index = "AB_TEST_1", + trafficPercentage = 50 + ), + AbTestsVariant( + index = "AB_TEST_2", + trafficPercentage = 50 + ) + ) + ) + ) + + // >LOG + // Use the response + val value = Await.result(response, Duration(100, "sec")) + - lang: swift + label: Swift + source: > + // Initialize the client + + let client = try AbtestingClient(appID: "ALGOLIA_APPLICATION_ID", + apiKey: "ALGOLIA_API_KEY", region: .us) + + + // Call the API + + let response = try await + client.estimateABTest(estimateABTestRequest: EstimateABTestRequest( + configuration: EstimateConfiguration( + emptySearch: EmptySearch(exclude: true), + minimumDetectableEffect: MinimumDetectableEffect( + size: 0.03, + metric: EffectMetric.conversionRate + ) + ), + variants: [ + AddABTestsVariant.abTestsVariant(AbTestsVariant(index: "AB_TEST_1", trafficPercentage: 50)), + AddABTestsVariant.abTestsVariant(AbTestsVariant(index: "AB_TEST_2", trafficPercentage: 50)), + ] + )) + + // >LOG + - lang: cURL + label: curl + source: |- + curl --request POST \ + --url https://analytics.us.algolia.com/2/abtests/estimate \ + --header 'accept: application/json' \ + --header 'content-type: application/json' \ + --header 'x-algolia-api-key: ALGOLIA_API_KEY' \ + --header 'x-algolia-application-id: ALGOLIA_APPLICATION_ID' \ + --data '{"configuration":{"outliers":{"exclude":true},"emptySearch":{"exclude":true},"minimumDetectableEffect":{"size":0,"metric":"addToCartRate"}},"variants":[{"index":"delcourt_production","trafficPercentage":60,"description":"Current production index"},{"index":"delcourt_production","trafficPercentage":60,"description":"Current production index"}]}' components: securitySchemes: appId: @@ -2920,7 +3350,7 @@ components: exclude: type: boolean description: Whether to exclude empty searches when calculating A/B test results. - Effect: + EffectMetric: type: string description: Metric for which you want to detect the smallest relative difference. enum: @@ -2944,8 +3374,11 @@ components: For example, to detect a 10% difference between variants, set this value to 0.1. - effect: - $ref: '#/components/schemas/Effect' + metric: + $ref: '#/components/schemas/EffectMetric' + required: + - size + - metric ABTestConfiguration: title: configuration type: object @@ -3150,6 +3583,31 @@ components: $ref: '#/components/schemas/abTestScheduleID' required: - abTestScheduleID + EstimateABTestResponse: + type: object + properties: + durationDays: + type: integer + format: int64 + description: >- + Estimated number of days needed to reach the sample sizes required + for detecting the configured effect. This value is based on + historical traffic. + example: 21 + controlSampleSize: + type: integer + format: int64 + description: >- + Number of tracked searches needed to be able to detect the + configured effect for the control variant. + example: 23415 + experimentSampleSize: + type: integer + format: int64 + description: >- + Number of tracked searches needed to be able to detect the + configured effect for the experiment variant. + example: 23415 responses: BadRequest: description: Bad request or request arguments. diff --git a/specs/bundled/abtesting.yml b/specs/bundled/abtesting.yml index 509cdcdf261..be3982f2b10 100644 --- a/specs/bundled/abtesting.yml +++ b/specs/bundled/abtesting.yml @@ -107,8 +107,8 @@ tags: Manage A/B tests. - A/B tests are configurations of two indices, usually your production index - and an index with different settings that you want to test. + A/B tests are configurations one or more indices, usually your production + index and an index with different settings that you want to test. externalDocs: url: https://www.algolia.com/doc/guides/ab-testing/what-is-ab-testing/ description: | @@ -517,6 +517,74 @@ paths: $ref: '#/components/responses/MethodNotAllowed' '404': $ref: '#/components/responses/IndexNotFound' + /2/abtests/estimate: + post: + tags: + - abtesting + operationId: estimateABTest + x-acl: + - analytics + summary: Estimate the sample size and duration of an A/B test + description: >- + Given the traffic percentage and the expected effect size, this endpoint + estimates the sample size and duration of an A/B test based on + historical traffic. + requestBody: + required: true + content: + application/json: + schema: + title: estimateABTestRequest + type: object + additionalProperties: false + properties: + configuration: + title: estimateConfiguration + type: object + description: >- + A/B test configuration for estimating the sample size and + duration using minimum detectable effect. + properties: + outliers: + $ref: '#/components/schemas/Outliers' + emptySearch: + $ref: '#/components/schemas/EmptySearch' + minimumDetectableEffect: + $ref: '#/components/schemas/MinimumDetectableEffect' + required: + - minimumDetectableEffect + variants: + type: array + description: A/B test variants. + minItems: 2 + maxItems: 2 + items: + $ref: '#/components/schemas/AddABTestsVariant' + required: + - configuration + - variants + responses: + '200': + description: OK + headers: + x-ratelimit-limit: + $ref: '#/components/headers/x-ratelimit-limit' + x-ratelimit-remaining: + $ref: '#/components/headers/x-ratelimit-remaining' + x-ratelimit-reset: + $ref: '#/components/headers/x-ratelimit-reset' + content: + application/json: + schema: + $ref: '#/components/schemas/EstimateABTestResponse' + '400': + $ref: '#/components/responses/BadRequest' + '402': + $ref: '#/components/responses/FeatureNotEnabled' + '403': + $ref: '#/components/responses/MethodNotAllowed' + '404': + $ref: '#/components/responses/IndexNotFound' /setClientApiKey: get: x-helper: true @@ -888,7 +956,7 @@ components: exclude: type: boolean description: Whether to exclude empty searches when calculating A/B test results. - Effect: + EffectMetric: type: string description: Metric for which you want to detect the smallest relative difference. enum: @@ -912,8 +980,11 @@ components: For example, to detect a 10% difference between variants, set this value to 0.1. - effect: - $ref: '#/components/schemas/Effect' + metric: + $ref: '#/components/schemas/EffectMetric' + required: + - size + - metric ABTestConfiguration: title: configuration type: object @@ -1118,6 +1189,31 @@ components: $ref: '#/components/schemas/abTestScheduleID' required: - abTestScheduleID + EstimateABTestResponse: + type: object + properties: + durationDays: + type: integer + format: int64 + description: >- + Estimated number of days needed to reach the sample sizes required + for detecting the configured effect. This value is based on + historical traffic. + example: 21 + controlSampleSize: + type: integer + format: int64 + description: >- + Number of tracked searches needed to be able to detect the + configured effect for the control variant. + example: 23415 + experimentSampleSize: + type: integer + format: int64 + description: >- + Number of tracked searches needed to be able to detect the + configured effect for the experiment variant. + example: 23415 responses: BadRequest: description: Bad request or request arguments. diff --git a/tests/output/csharp/src/generated/e2e/Insights.test.cs b/tests/output/csharp/src/generated/e2e/Insights.test.cs index dddfcd8738b..a24148bed62 100644 --- a/tests/output/csharp/src/generated/e2e/Insights.test.cs +++ b/tests/output/csharp/src/generated/e2e/Insights.test.cs @@ -63,7 +63,7 @@ public async Task PushEventsTest1() Index = "products", UserToken = "user-123456", AuthenticatedUserToken = "user-123456", - Timestamp = 1730678400000L, + Timestamp = 1730937600000L, ObjectIDs = new List { "9780545139700", "9780439784542" }, QueryID = "43b15df305339e827f0ac0bdc5ebcaa7", } @@ -76,7 +76,7 @@ public async Task PushEventsTest1() Index = "products", UserToken = "user-123456", AuthenticatedUserToken = "user-123456", - Timestamp = 1730678400000L, + Timestamp = 1730937600000L, ObjectIDs = new List { "9780545139700", "9780439784542" }, } ), diff --git a/tests/output/csharp/src/generated/requests/Abtesting.test.cs b/tests/output/csharp/src/generated/requests/Abtesting.test.cs index c7936d3c794..dce1f4fe6f6 100644 --- a/tests/output/csharp/src/generated/requests/Abtesting.test.cs +++ b/tests/output/csharp/src/generated/requests/Abtesting.test.cs @@ -554,6 +554,39 @@ public async Task DeleteABTestTest() Assert.Null(req.Body); } + [Fact(DisplayName = "estimate AB Test sample size")] + public async Task EstimateABTestTest() + { + await client.EstimateABTestAsync( + new EstimateABTestRequest + { + Configuration = new EstimateConfiguration + { + EmptySearch = new EmptySearch { Exclude = true }, + MinimumDetectableEffect = new MinimumDetectableEffect + { + Size = 0.03, + Metric = Enum.Parse("ConversionRate"), + }, + }, + Variants = new List + { + new AddABTestsVariant(new AbTestsVariant { Index = "AB_TEST_1", TrafficPercentage = 50 }), + new AddABTestsVariant(new AbTestsVariant { Index = "AB_TEST_2", TrafficPercentage = 50 }), + }, + } + ); + + var req = _echo.LastResponse; + Assert.Equal("/2/abtests/estimate", req.Path); + Assert.Equal("POST", req.Method.ToString()); + JsonAssert.EqualOverrideDefault( + "{\"configuration\":{\"emptySearch\":{\"exclude\":true},\"minimumDetectableEffect\":{\"size\":0.03,\"metric\":\"conversionRate\"}},\"variants\":[{\"index\":\"AB_TEST_1\",\"trafficPercentage\":50},{\"index\":\"AB_TEST_2\",\"trafficPercentage\":50}]}", + req.Body, + new JsonDiffConfig(false) + ); + } + [Fact(DisplayName = "getABTest")] public async Task GetABTestTest() { diff --git a/tests/output/csharp/src/generated/requests/Insights.test.cs b/tests/output/csharp/src/generated/requests/Insights.test.cs index ff71c5ea05f..d0cb7211791 100644 --- a/tests/output/csharp/src/generated/requests/Insights.test.cs +++ b/tests/output/csharp/src/generated/requests/Insights.test.cs @@ -580,7 +580,7 @@ await client.PushEventsAsync( Index = "products", UserToken = "user-123456", AuthenticatedUserToken = "user-123456", - Timestamp = 1730678400000L, + Timestamp = 1730937600000L, ObjectIDs = new List { "9780545139700", "9780439784542" }, QueryID = "43b15df305339e827f0ac0bdc5ebcaa7", } @@ -593,7 +593,7 @@ await client.PushEventsAsync( Index = "products", UserToken = "user-123456", AuthenticatedUserToken = "user-123456", - Timestamp = 1730678400000L, + Timestamp = 1730937600000L, ObjectIDs = new List { "9780545139700", "9780439784542" }, } ), @@ -605,7 +605,7 @@ await client.PushEventsAsync( Assert.Equal("/1/events", req.Path); Assert.Equal("POST", req.Method.ToString()); JsonAssert.EqualOverrideDefault( - "{\"events\":[{\"eventType\":\"conversion\",\"eventName\":\"Product Purchased\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730678400000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"],\"queryID\":\"43b15df305339e827f0ac0bdc5ebcaa7\"},{\"eventType\":\"view\",\"eventName\":\"Product Detail Page Viewed\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730678400000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"]}]}", + "{\"events\":[{\"eventType\":\"conversion\",\"eventName\":\"Product Purchased\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730937600000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"],\"queryID\":\"43b15df305339e827f0ac0bdc5ebcaa7\"},{\"eventType\":\"view\",\"eventName\":\"Product Detail Page Viewed\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730937600000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"]}]}", req.Body, new JsonDiffConfig(false) ); diff --git a/tests/output/dart/test/requests/insights_test.dart b/tests/output/dart/test/requests/insights_test.dart index c837386697e..9e97a5579f9 100644 --- a/tests/output/dart/test/requests/insights_test.dart +++ b/tests/output/dart/test/requests/insights_test.dart @@ -635,7 +635,7 @@ void main() { index: "products", userToken: "user-123456", authenticatedUserToken: "user-123456", - timestamp: 1730678400000, + timestamp: 1730937600000, objectIDs: [ "9780545139700", "9780439784542", @@ -648,7 +648,7 @@ void main() { index: "products", userToken: "user-123456", authenticatedUserToken: "user-123456", - timestamp: 1730678400000, + timestamp: 1730937600000, objectIDs: [ "9780545139700", "9780439784542", @@ -661,7 +661,7 @@ void main() { expectPath(request.path, '/1/events'); expect(request.method, 'post'); expectBody(request.body, - """{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"]}]}"""); + """{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"]}]}"""); }, ), ); diff --git a/tests/output/go/tests/e2e/insights_test.go b/tests/output/go/tests/e2e/insights_test.go index 2d02ce62880..79350fdda92 100644 --- a/tests/output/go/tests/e2e/insights_test.go +++ b/tests/output/go/tests/e2e/insights_test.go @@ -38,9 +38,9 @@ func TestInsightsE2E_PushEvents(t *testing.T) { insights.NewEmptyInsightsEvents().SetEvents( []insights.EventsItems{*insights.ConvertedObjectIDsAfterSearchAsEventsItems( - insights.NewEmptyConvertedObjectIDsAfterSearch().SetEventType(insights.ConversionEvent("conversion")).SetEventName("Product Purchased").SetIndex("products").SetUserToken("user-123456").SetAuthenticatedUserToken("user-123456").SetTimestamp(1730678400000).SetObjectIDs( + insights.NewEmptyConvertedObjectIDsAfterSearch().SetEventType(insights.ConversionEvent("conversion")).SetEventName("Product Purchased").SetIndex("products").SetUserToken("user-123456").SetAuthenticatedUserToken("user-123456").SetTimestamp(1730937600000).SetObjectIDs( []string{"9780545139700", "9780439784542"}).SetQueryID("43b15df305339e827f0ac0bdc5ebcaa7")), *insights.ViewedObjectIDsAsEventsItems( - insights.NewEmptyViewedObjectIDs().SetEventType(insights.ViewEvent("view")).SetEventName("Product Detail Page Viewed").SetIndex("products").SetUserToken("user-123456").SetAuthenticatedUserToken("user-123456").SetTimestamp(1730678400000).SetObjectIDs( + insights.NewEmptyViewedObjectIDs().SetEventType(insights.ViewEvent("view")).SetEventName("Product Detail Page Viewed").SetIndex("products").SetUserToken("user-123456").SetAuthenticatedUserToken("user-123456").SetTimestamp(1730937600000).SetObjectIDs( []string{"9780545139700", "9780439784542"}))}), )) require.NoError(t, err) diff --git a/tests/output/go/tests/requests/abtesting_test.go b/tests/output/go/tests/requests/abtesting_test.go index b521be59cb9..87bb2bc2a37 100644 --- a/tests/output/go/tests/requests/abtesting_test.go +++ b/tests/output/go/tests/requests/abtesting_test.go @@ -429,6 +429,31 @@ func TestAbtesting_DeleteABTest(t *testing.T) { }) } +func TestAbtesting_EstimateABTest(t *testing.T) { + client, echo := createAbtestingClient(t) + _ = echo + + t.Run("estimate AB Test sample size", func(t *testing.T) { + _, err := client.EstimateABTest(client.NewApiEstimateABTestRequest( + + abtesting.NewEmptyEstimateABTestRequest().SetConfiguration( + abtesting.NewEmptyEstimateConfiguration().SetEmptySearch( + abtesting.NewEmptyEmptySearch().SetExclude(true)).SetMinimumDetectableEffect( + abtesting.NewEmptyMinimumDetectableEffect().SetSize(0.03).SetMetric(abtesting.EffectMetric("conversionRate")))).SetVariants( + []abtesting.AddABTestsVariant{*abtesting.AbTestsVariantAsAddABTestsVariant( + abtesting.NewEmptyAbTestsVariant().SetIndex("AB_TEST_1").SetTrafficPercentage(50)), *abtesting.AbTestsVariantAsAddABTestsVariant( + abtesting.NewEmptyAbTestsVariant().SetIndex("AB_TEST_2").SetTrafficPercentage(50))}), + )) + require.NoError(t, err) + + require.Equal(t, "/2/abtests/estimate", echo.Path) + require.Equal(t, "POST", echo.Method) + + ja := jsonassert.New(t) + ja.Assertf(*echo.Body, `{"configuration":{"emptySearch":{"exclude":true},"minimumDetectableEffect":{"size":0.03,"metric":"conversionRate"}},"variants":[{"index":"AB_TEST_1","trafficPercentage":50},{"index":"AB_TEST_2","trafficPercentage":50}]}`) + }) +} + func TestAbtesting_GetABTest(t *testing.T) { client, echo := createAbtestingClient(t) _ = echo diff --git a/tests/output/go/tests/requests/insights_test.go b/tests/output/go/tests/requests/insights_test.go index be3bbbecea0..37f00548584 100644 --- a/tests/output/go/tests/requests/insights_test.go +++ b/tests/output/go/tests/requests/insights_test.go @@ -433,9 +433,9 @@ func TestInsights_PushEvents(t *testing.T) { insights.NewEmptyInsightsEvents().SetEvents( []insights.EventsItems{*insights.ConvertedObjectIDsAfterSearchAsEventsItems( - insights.NewEmptyConvertedObjectIDsAfterSearch().SetEventType(insights.ConversionEvent("conversion")).SetEventName("Product Purchased").SetIndex("products").SetUserToken("user-123456").SetAuthenticatedUserToken("user-123456").SetTimestamp(1730678400000).SetObjectIDs( + insights.NewEmptyConvertedObjectIDsAfterSearch().SetEventType(insights.ConversionEvent("conversion")).SetEventName("Product Purchased").SetIndex("products").SetUserToken("user-123456").SetAuthenticatedUserToken("user-123456").SetTimestamp(1730937600000).SetObjectIDs( []string{"9780545139700", "9780439784542"}).SetQueryID("43b15df305339e827f0ac0bdc5ebcaa7")), *insights.ViewedObjectIDsAsEventsItems( - insights.NewEmptyViewedObjectIDs().SetEventType(insights.ViewEvent("view")).SetEventName("Product Detail Page Viewed").SetIndex("products").SetUserToken("user-123456").SetAuthenticatedUserToken("user-123456").SetTimestamp(1730678400000).SetObjectIDs( + insights.NewEmptyViewedObjectIDs().SetEventType(insights.ViewEvent("view")).SetEventName("Product Detail Page Viewed").SetIndex("products").SetUserToken("user-123456").SetAuthenticatedUserToken("user-123456").SetTimestamp(1730937600000).SetObjectIDs( []string{"9780545139700", "9780439784542"}))}), )) require.NoError(t, err) @@ -444,7 +444,7 @@ func TestInsights_PushEvents(t *testing.T) { require.Equal(t, "POST", echo.Method) ja := jsonassert.New(t) - ja.Assertf(*echo.Body, `{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"]}]}`) + ja.Assertf(*echo.Body, `{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"]}]}`) }) t.Run("ConvertedObjectIDsAfterSearch", func(t *testing.T) { _, err := client.PushEvents(client.NewApiPushEventsRequest( diff --git a/tests/output/java/src/test/java/com/algolia/e2e/Insights.test.java b/tests/output/java/src/test/java/com/algolia/e2e/Insights.test.java index a72a7012321..01043aa5234 100644 --- a/tests/output/java/src/test/java/com/algolia/e2e/Insights.test.java +++ b/tests/output/java/src/test/java/com/algolia/e2e/Insights.test.java @@ -50,7 +50,7 @@ void pushEventsTest1() { .setIndex("products") .setUserToken("user-123456") .setAuthenticatedUserToken("user-123456") - .setTimestamp(1730678400000L) + .setTimestamp(1730937600000L) .setObjectIDs(Arrays.asList("9780545139700", "9780439784542")) .setQueryID("43b15df305339e827f0ac0bdc5ebcaa7"), new ViewedObjectIDs() @@ -59,7 +59,7 @@ void pushEventsTest1() { .setIndex("products") .setUserToken("user-123456") .setAuthenticatedUserToken("user-123456") - .setTimestamp(1730678400000L) + .setTimestamp(1730937600000L) .setObjectIDs(Arrays.asList("9780545139700", "9780439784542")) ) ) diff --git a/tests/output/java/src/test/java/com/algolia/requests/Abtesting.test.java b/tests/output/java/src/test/java/com/algolia/requests/Abtesting.test.java index 6c931aa021f..a9cf209f54a 100644 --- a/tests/output/java/src/test/java/com/algolia/requests/Abtesting.test.java +++ b/tests/output/java/src/test/java/com/algolia/requests/Abtesting.test.java @@ -699,6 +699,37 @@ void deleteABTestTest() { assertNull(req.body); } + @Test + @DisplayName("estimate AB Test sample size") + void estimateABTestTest() { + assertDoesNotThrow(() -> { + client.estimateABTest( + new EstimateABTestRequest() + .setConfiguration( + new EstimateConfiguration() + .setEmptySearch(new EmptySearch().setExclude(true)) + .setMinimumDetectableEffect(new MinimumDetectableEffect().setSize(0.03).setMetric(EffectMetric.CONVERSION_RATE)) + ) + .setVariants( + Arrays.asList( + new AbTestsVariant().setIndex("AB_TEST_1").setTrafficPercentage(50), + new AbTestsVariant().setIndex("AB_TEST_2").setTrafficPercentage(50) + ) + ) + ); + }); + EchoResponse req = echo.getLastResponse(); + assertEquals("/2/abtests/estimate", req.path); + assertEquals("POST", req.method); + assertDoesNotThrow(() -> + JSONAssert.assertEquals( + "{\"configuration\":{\"emptySearch\":{\"exclude\":true},\"minimumDetectableEffect\":{\"size\":0.03,\"metric\":\"conversionRate\"}},\"variants\":[{\"index\":\"AB_TEST_1\",\"trafficPercentage\":50},{\"index\":\"AB_TEST_2\",\"trafficPercentage\":50}]}", + req.body, + JSONCompareMode.STRICT + ) + ); + } + @Test @DisplayName("getABTest") void getABTestTest() { diff --git a/tests/output/java/src/test/java/com/algolia/requests/Insights.test.java b/tests/output/java/src/test/java/com/algolia/requests/Insights.test.java index 967dd13aefc..bca4345f9c8 100644 --- a/tests/output/java/src/test/java/com/algolia/requests/Insights.test.java +++ b/tests/output/java/src/test/java/com/algolia/requests/Insights.test.java @@ -720,7 +720,7 @@ void pushEventsTest1() { .setIndex("products") .setUserToken("user-123456") .setAuthenticatedUserToken("user-123456") - .setTimestamp(1730678400000L) + .setTimestamp(1730937600000L) .setObjectIDs(Arrays.asList("9780545139700", "9780439784542")) .setQueryID("43b15df305339e827f0ac0bdc5ebcaa7"), new ViewedObjectIDs() @@ -729,7 +729,7 @@ void pushEventsTest1() { .setIndex("products") .setUserToken("user-123456") .setAuthenticatedUserToken("user-123456") - .setTimestamp(1730678400000L) + .setTimestamp(1730937600000L) .setObjectIDs(Arrays.asList("9780545139700", "9780439784542")) ) ) @@ -741,9 +741,9 @@ void pushEventsTest1() { assertDoesNotThrow(() -> JSONAssert.assertEquals( "{\"events\":[{\"eventType\":\"conversion\",\"eventName\":\"Product" + - " Purchased\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730678400000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"],\"queryID\":\"43b15df305339e827f0ac0bdc5ebcaa7\"},{\"eventType\":\"view\",\"eventName\":\"Product" + + " Purchased\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730937600000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"],\"queryID\":\"43b15df305339e827f0ac0bdc5ebcaa7\"},{\"eventType\":\"view\",\"eventName\":\"Product" + " Detail Page" + - " Viewed\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730678400000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"]}]}", + " Viewed\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730937600000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"]}]}", req.body, JSONCompareMode.STRICT ) diff --git a/tests/output/javascript/src/e2e/insights.test.ts b/tests/output/javascript/src/e2e/insights.test.ts index dd47f192471..f460f1e2d28 100644 --- a/tests/output/javascript/src/e2e/insights.test.ts +++ b/tests/output/javascript/src/e2e/insights.test.ts @@ -30,7 +30,7 @@ describe('pushEvents', () => { index: 'products', userToken: 'user-123456', authenticatedUserToken: 'user-123456', - timestamp: 1730678400000, + timestamp: 1730937600000, objectIDs: ['9780545139700', '9780439784542'], queryID: '43b15df305339e827f0ac0bdc5ebcaa7', }, @@ -40,7 +40,7 @@ describe('pushEvents', () => { index: 'products', userToken: 'user-123456', authenticatedUserToken: 'user-123456', - timestamp: 1730678400000, + timestamp: 1730937600000, objectIDs: ['9780545139700', '9780439784542'], }, ], diff --git a/tests/output/javascript/src/requests/abtesting.test.ts b/tests/output/javascript/src/requests/abtesting.test.ts index cb33067d620..fd2fa2783e8 100644 --- a/tests/output/javascript/src/requests/abtesting.test.ts +++ b/tests/output/javascript/src/requests/abtesting.test.ts @@ -289,6 +289,35 @@ describe('deleteABTest', () => { }); }); +describe('estimateABTest', () => { + test('estimate AB Test sample size', async () => { + const req = (await client.estimateABTest({ + configuration: { + emptySearch: { exclude: true }, + minimumDetectableEffect: { size: 0.03, metric: 'conversionRate' }, + }, + variants: [ + { index: 'AB_TEST_1', trafficPercentage: 50 }, + { index: 'AB_TEST_2', trafficPercentage: 50 }, + ], + })) as unknown as EchoResponse; + + expect(req.path).toEqual('/2/abtests/estimate'); + expect(req.method).toEqual('POST'); + expect(req.data).toEqual({ + configuration: { + emptySearch: { exclude: true }, + minimumDetectableEffect: { size: 0.03, metric: 'conversionRate' }, + }, + variants: [ + { index: 'AB_TEST_1', trafficPercentage: 50 }, + { index: 'AB_TEST_2', trafficPercentage: 50 }, + ], + }); + expect(req.searchParams).toStrictEqual(undefined); + }); +}); + describe('getABTest', () => { test('getABTest', async () => { const req = (await client.getABTest({ id: 42 })) as unknown as EchoResponse; diff --git a/tests/output/javascript/src/requests/insights.test.ts b/tests/output/javascript/src/requests/insights.test.ts index e34c5456528..72c23876035 100644 --- a/tests/output/javascript/src/requests/insights.test.ts +++ b/tests/output/javascript/src/requests/insights.test.ts @@ -311,7 +311,7 @@ describe('pushEvents', () => { index: 'products', userToken: 'user-123456', authenticatedUserToken: 'user-123456', - timestamp: 1730678400000, + timestamp: 1730937600000, objectIDs: ['9780545139700', '9780439784542'], queryID: '43b15df305339e827f0ac0bdc5ebcaa7', }, @@ -321,7 +321,7 @@ describe('pushEvents', () => { index: 'products', userToken: 'user-123456', authenticatedUserToken: 'user-123456', - timestamp: 1730678400000, + timestamp: 1730937600000, objectIDs: ['9780545139700', '9780439784542'], }, ], @@ -337,7 +337,7 @@ describe('pushEvents', () => { index: 'products', userToken: 'user-123456', authenticatedUserToken: 'user-123456', - timestamp: 1730678400000, + timestamp: 1730937600000, objectIDs: ['9780545139700', '9780439784542'], queryID: '43b15df305339e827f0ac0bdc5ebcaa7', }, @@ -347,7 +347,7 @@ describe('pushEvents', () => { index: 'products', userToken: 'user-123456', authenticatedUserToken: 'user-123456', - timestamp: 1730678400000, + timestamp: 1730937600000, objectIDs: ['9780545139700', '9780439784542'], }, ], diff --git a/tests/output/kotlin/src/commonTest/kotlin/com/algolia/e2e/InsightsTest.kt b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/e2e/InsightsTest.kt index 43de16d0f12..57170a12868 100644 --- a/tests/output/kotlin/src/commonTest/kotlin/com/algolia/e2e/InsightsTest.kt +++ b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/e2e/InsightsTest.kt @@ -41,7 +41,7 @@ class InsightsTest { index = "products", userToken = "user-123456", authenticatedUserToken = "user-123456", - timestamp = 1730678400000L, + timestamp = 1730937600000L, objectIDs = listOf("9780545139700", "9780439784542"), queryID = "43b15df305339e827f0ac0bdc5ebcaa7", ), @@ -51,7 +51,7 @@ class InsightsTest { index = "products", userToken = "user-123456", authenticatedUserToken = "user-123456", - timestamp = 1730678400000L, + timestamp = 1730937600000L, objectIDs = listOf("9780545139700", "9780439784542"), ), ), diff --git a/tests/output/kotlin/src/commonTest/kotlin/com/algolia/requests/AbtestingTest.kt b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/requests/AbtestingTest.kt index 5d514d53232..3aa073480f7 100644 --- a/tests/output/kotlin/src/commonTest/kotlin/com/algolia/requests/AbtestingTest.kt +++ b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/requests/AbtestingTest.kt @@ -515,6 +515,44 @@ class AbtestingTest { ) } + // estimateABTest + + @Test + fun `estimate AB Test sample size`() = runTest { + client.runTest( + call = { + estimateABTest( + estimateABTestRequest = EstimateABTestRequest( + configuration = EstimateConfiguration( + emptySearch = EmptySearch( + exclude = true, + ), + minimumDetectableEffect = MinimumDetectableEffect( + size = 0.03, + metric = EffectMetric.entries.first { it.value == "conversionRate" }, + ), + ), + variants = listOf( + AbTestsVariant( + index = "AB_TEST_1", + trafficPercentage = 50, + ), + AbTestsVariant( + index = "AB_TEST_2", + trafficPercentage = 50, + ), + ), + ), + ) + }, + intercept = { + assertEquals("/2/abtests/estimate".toPathSegments(), it.url.pathSegments) + assertEquals(HttpMethod.parse("POST"), it.method) + assertJsonBody("""{"configuration":{"emptySearch":{"exclude":true},"minimumDetectableEffect":{"size":0.03,"metric":"conversionRate"}},"variants":[{"index":"AB_TEST_1","trafficPercentage":50},{"index":"AB_TEST_2","trafficPercentage":50}]}""", it.body) + }, + ) + } + // getABTest @Test diff --git a/tests/output/kotlin/src/commonTest/kotlin/com/algolia/requests/InsightsTest.kt b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/requests/InsightsTest.kt index f6c046f4d8a..79b3e653a14 100644 --- a/tests/output/kotlin/src/commonTest/kotlin/com/algolia/requests/InsightsTest.kt +++ b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/requests/InsightsTest.kt @@ -529,7 +529,7 @@ class InsightsTest { index = "products", userToken = "user-123456", authenticatedUserToken = "user-123456", - timestamp = 1730678400000L, + timestamp = 1730937600000L, objectIDs = listOf("9780545139700", "9780439784542"), queryID = "43b15df305339e827f0ac0bdc5ebcaa7", ), @@ -539,7 +539,7 @@ class InsightsTest { index = "products", userToken = "user-123456", authenticatedUserToken = "user-123456", - timestamp = 1730678400000L, + timestamp = 1730937600000L, objectIDs = listOf("9780545139700", "9780439784542"), ), ), @@ -549,7 +549,7 @@ class InsightsTest { intercept = { assertEquals("/1/events".toPathSegments(), it.url.pathSegments) assertEquals(HttpMethod.parse("POST"), it.method) - assertJsonBody("""{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"]}]}""", it.body) + assertJsonBody("""{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"]}]}""", it.body) }, ) } diff --git a/tests/output/php/src/e2e/InsightsTest.php b/tests/output/php/src/e2e/InsightsTest.php index 3781bef397a..2b2697bb0c0 100644 --- a/tests/output/php/src/e2e/InsightsTest.php +++ b/tests/output/php/src/e2e/InsightsTest.php @@ -35,7 +35,7 @@ public function testPushEvents1(): void 'index' => 'products', 'userToken' => 'user-123456', 'authenticatedUserToken' => 'user-123456', - 'timestamp' => 1730678400000, + 'timestamp' => 1730937600000, 'objectIDs' => [ '9780545139700', @@ -49,7 +49,7 @@ public function testPushEvents1(): void 'index' => 'products', 'userToken' => 'user-123456', 'authenticatedUserToken' => 'user-123456', - 'timestamp' => 1730678400000, + 'timestamp' => 1730937600000, 'objectIDs' => [ '9780545139700', diff --git a/tests/output/php/src/requests/AbtestingTest.php b/tests/output/php/src/requests/AbtestingTest.php index 0122df1ed68..a7e0e99db49 100644 --- a/tests/output/php/src/requests/AbtestingTest.php +++ b/tests/output/php/src/requests/AbtestingTest.php @@ -507,6 +507,38 @@ public function testDeleteABTest(): void ]); } + #[TestDox('estimate AB Test sample size')] + public function testEstimateABTest(): void + { + $client = $this->getClient(); + $client->estimateABTest( + ['configuration' => ['emptySearch' => ['exclude' => true, + ], + 'minimumDetectableEffect' => ['size' => 0.03, + 'metric' => 'conversionRate', + ], + ], + 'variants' => [ + ['index' => 'AB_TEST_1', + 'trafficPercentage' => 50, + ], + + ['index' => 'AB_TEST_2', + 'trafficPercentage' => 50, + ], + ], + ], + ); + + $this->assertRequests([ + [ + 'path' => '/2/abtests/estimate', + 'method' => 'POST', + 'body' => json_decode('{"configuration":{"emptySearch":{"exclude":true},"minimumDetectableEffect":{"size":0.03,"metric":"conversionRate"}},"variants":[{"index":"AB_TEST_1","trafficPercentage":50},{"index":"AB_TEST_2","trafficPercentage":50}]}'), + ], + ]); + } + #[TestDox('getABTest')] public function testGetABTest(): void { diff --git a/tests/output/php/src/requests/InsightsTest.php b/tests/output/php/src/requests/InsightsTest.php index 63d39598afa..c60d423071a 100644 --- a/tests/output/php/src/requests/InsightsTest.php +++ b/tests/output/php/src/requests/InsightsTest.php @@ -527,7 +527,7 @@ public function testPushEvents1(): void 'index' => 'products', 'userToken' => 'user-123456', 'authenticatedUserToken' => 'user-123456', - 'timestamp' => 1730678400000, + 'timestamp' => 1730937600000, 'objectIDs' => [ '9780545139700', @@ -541,7 +541,7 @@ public function testPushEvents1(): void 'index' => 'products', 'userToken' => 'user-123456', 'authenticatedUserToken' => 'user-123456', - 'timestamp' => 1730678400000, + 'timestamp' => 1730937600000, 'objectIDs' => [ '9780545139700', @@ -556,7 +556,7 @@ public function testPushEvents1(): void [ 'path' => '/1/events', 'method' => 'POST', - 'body' => json_decode('{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"]}]}'), + 'body' => json_decode('{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"]}]}'), ], ]); } diff --git a/tests/output/python/tests/e2e/insights_test.py b/tests/output/python/tests/e2e/insights_test.py index 8a1781f616e..ac673d72f20 100644 --- a/tests/output/python/tests/e2e/insights_test.py +++ b/tests/output/python/tests/e2e/insights_test.py @@ -36,7 +36,7 @@ async def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -49,7 +49,7 @@ async def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -71,7 +71,7 @@ async def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -84,7 +84,7 @@ async def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -127,7 +127,7 @@ def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -140,7 +140,7 @@ def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -162,7 +162,7 @@ def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -175,7 +175,7 @@ def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", diff --git a/tests/output/python/tests/requests/abtesting_test.py b/tests/output/python/tests/requests/abtesting_test.py index 56ef6a7100f..87e575bf6eb 100644 --- a/tests/output/python/tests/requests/abtesting_test.py +++ b/tests/output/python/tests/requests/abtesting_test.py @@ -443,6 +443,42 @@ async def test_delete_ab_test_(self): assert _req.headers.items() >= {}.items() assert _req.data is None + async def test_estimate_ab_test_(self): + """ + estimate AB Test sample size + """ + _req = await self._client.estimate_ab_test_with_http_info( + estimate_ab_test_request={ + "configuration": { + "emptySearch": { + "exclude": True, + }, + "minimumDetectableEffect": { + "size": 0.03, + "metric": "conversionRate", + }, + }, + "variants": [ + { + "index": "AB_TEST_1", + "trafficPercentage": 50, + }, + { + "index": "AB_TEST_2", + "trafficPercentage": 50, + }, + ], + }, + ) + + assert _req.path == "/2/abtests/estimate" + assert _req.verb == "POST" + assert _req.query_parameters.items() == {}.items() + assert _req.headers.items() >= {}.items() + assert loads(_req.data) == loads( + """{"configuration":{"emptySearch":{"exclude":true},"minimumDetectableEffect":{"size":0.03,"metric":"conversionRate"}},"variants":[{"index":"AB_TEST_1","trafficPercentage":50},{"index":"AB_TEST_2","trafficPercentage":50}]}""" + ) + async def test_get_ab_test_(self): """ getABTest @@ -974,6 +1010,42 @@ def test_delete_ab_test_(self): assert _req.headers.items() >= {}.items() assert _req.data is None + def test_estimate_ab_test_(self): + """ + estimate AB Test sample size + """ + _req = self._client.estimate_ab_test_with_http_info( + estimate_ab_test_request={ + "configuration": { + "emptySearch": { + "exclude": True, + }, + "minimumDetectableEffect": { + "size": 0.03, + "metric": "conversionRate", + }, + }, + "variants": [ + { + "index": "AB_TEST_1", + "trafficPercentage": 50, + }, + { + "index": "AB_TEST_2", + "trafficPercentage": 50, + }, + ], + }, + ) + + assert _req.path == "/2/abtests/estimate" + assert _req.verb == "POST" + assert _req.query_parameters.items() == {}.items() + assert _req.headers.items() >= {}.items() + assert loads(_req.data) == loads( + """{"configuration":{"emptySearch":{"exclude":true},"minimumDetectableEffect":{"size":0.03,"metric":"conversionRate"}},"variants":[{"index":"AB_TEST_1","trafficPercentage":50},{"index":"AB_TEST_2","trafficPercentage":50}]}""" + ) + def test_get_ab_test_(self): """ getABTest diff --git a/tests/output/python/tests/requests/insights_test.py b/tests/output/python/tests/requests/insights_test.py index f0fa8da570d..f2ad7026911 100644 --- a/tests/output/python/tests/requests/insights_test.py +++ b/tests/output/python/tests/requests/insights_test.py @@ -463,7 +463,7 @@ async def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -476,7 +476,7 @@ async def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -491,7 +491,7 @@ async def test_push_events_1(self): assert _req.query_parameters.items() == {}.items() assert _req.headers.items() >= {}.items() assert loads(_req.data) == loads( - """{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"]}]}""" + """{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"]}]}""" ) async def test_push_events_2(self): @@ -1060,7 +1060,7 @@ def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -1073,7 +1073,7 @@ def test_push_events_1(self): "index": "products", "userToken": "user-123456", "authenticatedUserToken": "user-123456", - "timestamp": 1730678400000, + "timestamp": 1730937600000, "objectIDs": [ "9780545139700", "9780439784542", @@ -1088,7 +1088,7 @@ def test_push_events_1(self): assert _req.query_parameters.items() == {}.items() assert _req.headers.items() >= {}.items() assert loads(_req.data) == loads( - """{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"]}]}""" + """{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"]}]}""" ) def test_push_events_2(self): diff --git a/tests/output/ruby/test/e2e/insights_test.rb b/tests/output/ruby/test/e2e/insights_test.rb index 68b2d30e46b..82ae604d220 100644 --- a/tests/output/ruby/test/e2e/insights_test.rb +++ b/tests/output/ruby/test/e2e/insights_test.rb @@ -27,7 +27,7 @@ def test_push_events1 index: "products", user_token: "user-123456", authenticated_user_token: "user-123456", - timestamp: 1730678400000, + timestamp: 1730937600000, object_ids: ["9780545139700", "9780439784542"], query_id: "43b15df305339e827f0ac0bdc5ebcaa7" ), @@ -37,7 +37,7 @@ def test_push_events1 index: "products", user_token: "user-123456", authenticated_user_token: "user-123456", - timestamp: 1730678400000, + timestamp: 1730937600000, object_ids: ["9780545139700", "9780439784542"] ) ] @@ -54,7 +54,7 @@ def test_push_events1 index: "products", user_token: "user-123456", authenticated_user_token: "user-123456", - timestamp: 1730678400000, + timestamp: 1730937600000, object_ids: ["9780545139700", "9780439784542"], query_id: "43b15df305339e827f0ac0bdc5ebcaa7" ), @@ -64,7 +64,7 @@ def test_push_events1 index: "products", user_token: "user-123456", authenticated_user_token: "user-123456", - timestamp: 1730678400000, + timestamp: 1730937600000, object_ids: ["9780545139700", "9780439784542"] ) ] diff --git a/tests/output/ruby/test/requests/abtesting_test.rb b/tests/output/ruby/test/requests/abtesting_test.rb index 60c27d44564..3bfb9738ca9 100644 --- a/tests/output/ruby/test/requests/abtesting_test.rb +++ b/tests/output/ruby/test/requests/abtesting_test.rb @@ -319,6 +319,36 @@ def test_delete_ab_test assert(req.body.nil?, "body is not nil") end + # estimate AB Test sample size + def test_estimate_ab_test + req = @client.estimate_ab_test_with_http_info( + Algolia::Abtesting::EstimateABTestRequest.new( + configuration: Algolia::Abtesting::EstimateConfiguration.new( + empty_search: Algolia::Abtesting::EmptySearch.new(exclude: true), + minimum_detectable_effect: Algolia::Abtesting::MinimumDetectableEffect.new( + size: 0.03, + metric: "conversionRate" + ) + ), + variants: [ + Algolia::Abtesting::AbTestsVariant.new(index: "AB_TEST_1", traffic_percentage: 50), + Algolia::Abtesting::AbTestsVariant.new(index: "AB_TEST_2", traffic_percentage: 50) + ] + ) + ) + + assert_equal(:post, req.method) + assert_equal("/2/abtests/estimate", req.path) + assert_equal({}.to_a, req.query_params.to_a) + assert(({}.to_a - req.headers.to_a).empty?, req.headers.to_s) + assert_equal( + JSON.parse( + "{\"configuration\":{\"emptySearch\":{\"exclude\":true},\"minimumDetectableEffect\":{\"size\":0.03,\"metric\":\"conversionRate\"}},\"variants\":[{\"index\":\"AB_TEST_1\",\"trafficPercentage\":50},{\"index\":\"AB_TEST_2\",\"trafficPercentage\":50}]}" + ), + JSON.parse(req.body) + ) + end + # getABTest def test_get_ab_test req = @client.get_ab_test_with_http_info(42) diff --git a/tests/output/ruby/test/requests/insights_test.rb b/tests/output/ruby/test/requests/insights_test.rb index 77649a4afa1..aef1948f658 100644 --- a/tests/output/ruby/test/requests/insights_test.rb +++ b/tests/output/ruby/test/requests/insights_test.rb @@ -337,7 +337,7 @@ def test_push_events1 index: "products", user_token: "user-123456", authenticated_user_token: "user-123456", - timestamp: 1730678400000, + timestamp: 1730937600000, object_ids: ["9780545139700", "9780439784542"], query_id: "43b15df305339e827f0ac0bdc5ebcaa7" ), @@ -347,7 +347,7 @@ def test_push_events1 index: "products", user_token: "user-123456", authenticated_user_token: "user-123456", - timestamp: 1730678400000, + timestamp: 1730937600000, object_ids: ["9780545139700", "9780439784542"] ) ] @@ -360,7 +360,7 @@ def test_push_events1 assert(({}.to_a - req.headers.to_a).empty?, req.headers.to_s) assert_equal( JSON.parse( - "{\"events\":[{\"eventType\":\"conversion\",\"eventName\":\"Product Purchased\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730678400000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"],\"queryID\":\"43b15df305339e827f0ac0bdc5ebcaa7\"},{\"eventType\":\"view\",\"eventName\":\"Product Detail Page Viewed\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730678400000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"]}]}" + "{\"events\":[{\"eventType\":\"conversion\",\"eventName\":\"Product Purchased\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730937600000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"],\"queryID\":\"43b15df305339e827f0ac0bdc5ebcaa7\"},{\"eventType\":\"view\",\"eventName\":\"Product Detail Page Viewed\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730937600000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"]}]}" ), JSON.parse(req.body) ) diff --git a/tests/output/scala/src/test/scala/algoliasearch/e2e/InsightsTest.scala b/tests/output/scala/src/test/scala/algoliasearch/e2e/InsightsTest.scala index 1eb139e5856..63158573128 100644 --- a/tests/output/scala/src/test/scala/algoliasearch/e2e/InsightsTest.scala +++ b/tests/output/scala/src/test/scala/algoliasearch/e2e/InsightsTest.scala @@ -49,7 +49,7 @@ class InsightsTestE2E extends AnyFunSuite { index = "products", userToken = "user-123456", authenticatedUserToken = Some("user-123456"), - timestamp = Some(1730678400000L), + timestamp = Some(1730937600000L), objectIDs = Seq("9780545139700", "9780439784542"), queryID = "43b15df305339e827f0ac0bdc5ebcaa7" ), @@ -59,7 +59,7 @@ class InsightsTestE2E extends AnyFunSuite { index = "products", userToken = "user-123456", authenticatedUserToken = Some("user-123456"), - timestamp = Some(1730678400000L), + timestamp = Some(1730937600000L), objectIDs = Seq("9780545139700", "9780439784542") ) ) diff --git a/tests/output/scala/src/test/scala/algoliasearch/requests/AbtestingTest.scala b/tests/output/scala/src/test/scala/algoliasearch/requests/AbtestingTest.scala index f14a04c456c..9439d6bcbf2 100644 --- a/tests/output/scala/src/test/scala/algoliasearch/requests/AbtestingTest.scala +++ b/tests/output/scala/src/test/scala/algoliasearch/requests/AbtestingTest.scala @@ -562,6 +562,46 @@ class AbtestingTest extends AnyFunSuite { assert(res.body.isEmpty) } + test("estimate AB Test sample size") { + val (client, echo) = testClient() + val future = client.estimateABTest( + estimateABTestRequest = EstimateABTestRequest( + configuration = EstimateConfiguration( + emptySearch = Some( + EmptySearch( + exclude = Some(true) + ) + ), + minimumDetectableEffect = MinimumDetectableEffect( + size = 0.03, + metric = EffectMetric.withName("conversionRate") + ) + ), + variants = Seq( + AbTestsVariant( + index = "AB_TEST_1", + trafficPercentage = 50 + ), + AbTestsVariant( + index = "AB_TEST_2", + trafficPercentage = 50 + ) + ) + ) + ) + + Await.ready(future, Duration.Inf) + val res = echo.lastResponse.get + + assert(res.path == "/2/abtests/estimate") + assert(res.method == "POST") + val expectedBody = parse( + """{"configuration":{"emptySearch":{"exclude":true},"minimumDetectableEffect":{"size":0.03,"metric":"conversionRate"}},"variants":[{"index":"AB_TEST_1","trafficPercentage":50},{"index":"AB_TEST_2","trafficPercentage":50}]}""" + ) + val actualBody = parse(res.body.get) + assert(actualBody == expectedBody) + } + test("getABTest") { val (client, echo) = testClient() val future = client.getABTest( diff --git a/tests/output/scala/src/test/scala/algoliasearch/requests/InsightsTest.scala b/tests/output/scala/src/test/scala/algoliasearch/requests/InsightsTest.scala index 9d52d7bd8fb..ccc91f725f2 100644 --- a/tests/output/scala/src/test/scala/algoliasearch/requests/InsightsTest.scala +++ b/tests/output/scala/src/test/scala/algoliasearch/requests/InsightsTest.scala @@ -574,7 +574,7 @@ class InsightsTest extends AnyFunSuite { index = "products", userToken = "user-123456", authenticatedUserToken = Some("user-123456"), - timestamp = Some(1730678400000L), + timestamp = Some(1730937600000L), objectIDs = Seq("9780545139700", "9780439784542"), queryID = "43b15df305339e827f0ac0bdc5ebcaa7" ), @@ -584,7 +584,7 @@ class InsightsTest extends AnyFunSuite { index = "products", userToken = "user-123456", authenticatedUserToken = Some("user-123456"), - timestamp = Some(1730678400000L), + timestamp = Some(1730937600000L), objectIDs = Seq("9780545139700", "9780439784542") ) ) @@ -597,7 +597,7 @@ class InsightsTest extends AnyFunSuite { assert(res.path == "/1/events") assert(res.method == "POST") val expectedBody = parse( - """{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730678400000,"objectIDs":["9780545139700","9780439784542"]}]}""" + """{"events":[{"eventType":"conversion","eventName":"Product Purchased","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"],"queryID":"43b15df305339e827f0ac0bdc5ebcaa7"},{"eventType":"view","eventName":"Product Detail Page Viewed","index":"products","userToken":"user-123456","authenticatedUserToken":"user-123456","timestamp":1730937600000,"objectIDs":["9780545139700","9780439784542"]}]}""" ) val actualBody = parse(res.body.get) assert(actualBody == expectedBody) diff --git a/tests/output/swift/Tests/e2e/InsightsTests.swift b/tests/output/swift/Tests/e2e/InsightsTests.swift index 945b6613699..8b4a55ba88c 100644 --- a/tests/output/swift/Tests/e2e/InsightsTests.swift +++ b/tests/output/swift/Tests/e2e/InsightsTests.swift @@ -65,7 +65,7 @@ final class InsightsClientRequestsTestsE2E: XCTestCase { queryID: "43b15df305339e827f0ac0bdc5ebcaa7", userToken: "user-123456", authenticatedUserToken: "user-123456", - timestamp: Int64(1_730_678_400_000) + timestamp: Int64(1_730_937_600_000) )), EventsItems.viewedObjectIDs(ViewedObjectIDs( eventName: "Product Detail Page Viewed", @@ -74,7 +74,7 @@ final class InsightsClientRequestsTestsE2E: XCTestCase { objectIDs: ["9780545139700", "9780439784542"], userToken: "user-123456", authenticatedUserToken: "user-123456", - timestamp: Int64(1_730_678_400_000) + timestamp: Int64(1_730_937_600_000) )), ])) let responseBody = try XCTUnwrap(response.body) diff --git a/tests/output/swift/Tests/requests/AbtestingTests.swift b/tests/output/swift/Tests/requests/AbtestingTests.swift index 91cf53c9571..ec906b027a0 100644 --- a/tests/output/swift/Tests/requests/AbtestingTests.swift +++ b/tests/output/swift/Tests/requests/AbtestingTests.swift @@ -759,6 +759,48 @@ final class AbtestingClientRequestsTests: XCTestCase { XCTAssertNil(echoResponse.queryParameters) } + /// estimate AB Test sample size + func testEstimateABTestTest() async throws { + let configuration = try AbtestingClientConfiguration( + appID: AbtestingClientRequestsTests.APPLICATION_ID, + apiKey: AbtestingClientRequestsTests.API_KEY, + region: Region.us + ) + let transporter = Transporter(configuration: configuration, requestBuilder: EchoRequestBuilder()) + let client = AbtestingClient(configuration: configuration, transporter: transporter) + + let response = try await client.estimateABTestWithHTTPInfo(estimateABTestRequest: EstimateABTestRequest( + configuration: EstimateConfiguration( + emptySearch: EmptySearch(exclude: true), + minimumDetectableEffect: MinimumDetectableEffect( + size: 0.03, + metric: EffectMetric.conversionRate + ) + ), + variants: [ + AddABTestsVariant.abTestsVariant(AbTestsVariant(index: "AB_TEST_1", trafficPercentage: 50)), + AddABTestsVariant.abTestsVariant(AbTestsVariant(index: "AB_TEST_2", trafficPercentage: 50)), + ] + )) + let responseBodyData = try XCTUnwrap(response.bodyData) + let echoResponse = try CodableHelper.jsonDecoder.decode(EchoResponse.self, from: responseBodyData) + + let echoResponseBodyData = try XCTUnwrap(echoResponse.originalBodyData) + let echoResponseBodyJSON = try XCTUnwrap(echoResponseBodyData.jsonString) + + let expectedBodyData = + "{\"configuration\":{\"emptySearch\":{\"exclude\":true},\"minimumDetectableEffect\":{\"size\":0.03,\"metric\":\"conversionRate\"}},\"variants\":[{\"index\":\"AB_TEST_1\",\"trafficPercentage\":50},{\"index\":\"AB_TEST_2\",\"trafficPercentage\":50}]}" + .data(using: .utf8) + let expectedBodyJSON = try XCTUnwrap(expectedBodyData?.jsonString) + + XCTAssertEqual(echoResponseBodyJSON, expectedBodyJSON) + + XCTAssertEqual(echoResponse.path, "/2/abtests/estimate") + XCTAssertEqual(echoResponse.method, HTTPMethod.post) + + XCTAssertNil(echoResponse.queryParameters) + } + /// getABTest func testGetABTestTest() async throws { let configuration = try AbtestingClientConfiguration( diff --git a/tests/output/swift/Tests/requests/InsightsTests.swift b/tests/output/swift/Tests/requests/InsightsTests.swift index 1413f3107ae..b6d714462ea 100644 --- a/tests/output/swift/Tests/requests/InsightsTests.swift +++ b/tests/output/swift/Tests/requests/InsightsTests.swift @@ -785,7 +785,7 @@ final class InsightsClientRequestsTests: XCTestCase { queryID: "43b15df305339e827f0ac0bdc5ebcaa7", userToken: "user-123456", authenticatedUserToken: "user-123456", - timestamp: Int64(1_730_678_400_000) + timestamp: Int64(1_730_937_600_000) )), EventsItems.viewedObjectIDs(ViewedObjectIDs( eventName: "Product Detail Page Viewed", @@ -794,7 +794,7 @@ final class InsightsClientRequestsTests: XCTestCase { objectIDs: ["9780545139700", "9780439784542"], userToken: "user-123456", authenticatedUserToken: "user-123456", - timestamp: Int64(1_730_678_400_000) + timestamp: Int64(1_730_937_600_000) )), ])) let responseBodyData = try XCTUnwrap(response.bodyData) @@ -804,7 +804,7 @@ final class InsightsClientRequestsTests: XCTestCase { let echoResponseBodyJSON = try XCTUnwrap(echoResponseBodyData.jsonString) let expectedBodyData = - "{\"events\":[{\"eventType\":\"conversion\",\"eventName\":\"Product Purchased\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730678400000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"],\"queryID\":\"43b15df305339e827f0ac0bdc5ebcaa7\"},{\"eventType\":\"view\",\"eventName\":\"Product Detail Page Viewed\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730678400000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"]}]}" + "{\"events\":[{\"eventType\":\"conversion\",\"eventName\":\"Product Purchased\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730937600000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"],\"queryID\":\"43b15df305339e827f0ac0bdc5ebcaa7\"},{\"eventType\":\"view\",\"eventName\":\"Product Detail Page Viewed\",\"index\":\"products\",\"userToken\":\"user-123456\",\"authenticatedUserToken\":\"user-123456\",\"timestamp\":1730937600000,\"objectIDs\":[\"9780545139700\",\"9780439784542\"]}]}" .data(using: .utf8) let expectedBodyJSON = try XCTUnwrap(expectedBodyData?.jsonString)