-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Updating SDK to 0.5.2 spec #23
Conversation
@Norhaven, looks great, thanks for contributing such as substantial update! My one question is why you changed all of the double's to floats for things experiment weights? I'm not necessarily opposed to it, but I'm curious as to the reason. |
@Mastermindzh, @pascal89 any thoughts on this PR before I merge this in? |
@darklordzw I'll have a look tomorrow morning (CEST) and let you know 😊 |
@darklordzw Thanks for taking a look! The use of floats instead of doubles was primarily because it seemed like the typical floating point values needed by things like weights didn't require more than ~7 digits of precision to be effective, but I'm open to either way if there's a preference for doubles in particular. Thoughts? |
@darklordzw I'll schedule a review moment for tomorrow |
@Norhaven, gotcha. My original implementation used floats, but I ended up switching to doubles to be consistent with the other Growthbook sdks (this was originally a port of the Python version). I don't have a strong opinion either way, but I lean towards staying with doubles to stay consistent with what's out there already. |
@darklordzw Sure, makes sense to me. I've replaced the float usages with doubles now. The precision change caused a minor rounding difference in the GetEqualWeights test that differs from the spec, so I've called that out in there with a little more info and adjusted it slightly. Let me know if you have more questions on anything, thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall it looks good, I made several recommendations though.
continue; | ||
} | ||
|
||
json = json.Substring(5).Trim(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel this statement should be inside of the if statement.
Now you'll trim 5 characters no matter the result of the statement :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... not sure I follow, but added some more comments to hopefully clarify the reasoning behind doing it this way. Let me know if I missed the point. :)
GrowthBook/Api/HttpClientFactory.cs
Outdated
{ | ||
public class HttpClientFactory : IHttpClientFactory | ||
{ | ||
public static class ConfiguredClients |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would extract this and move it to a separate file since the scope isn't limited to just this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, moved it out now.
|
||
private readonly TestHttpClientFactory _httpClientFactory; | ||
private readonly GrowthBookConfigurationOptions _config; | ||
private readonly FeatureRefreshWorker _worker; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use the new keyword to get rid of the "hiding" errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, forgot to remove that from the base class. Thanks!
{ | ||
private const string ApiKey = "<AN API KEY GOES HERE>"; | ||
|
||
[Fact(Skip = "This is here as a shortcut for the purpose of debugging this SDK against the actual API and should not be used otherwise")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like including these things t.b.h
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was a little on the fence about that as well. Removed.
|
||
public class LoadFeaturesTests | ||
{ | ||
private const string ApiKey = "<AN API KEY GOES HERE>"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're including this file I'd recommend using IConfiguration for these things:
See, this for an example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File is removed now.
GrowthBook/AssemblyInfo.cs
Outdated
@@ -0,0 +1,6 @@ | |||
using System.Runtime.CompilerServices; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest removing this file altogether and stick with the portable configuration in csproj:
<ItemGroup>
<InternalsVisibleTo Include="GrowthBook.Tests" />
</ItemGroup>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, forgot about that. Done.
/// </remarks> | ||
/// <param name="value">The string to verify.</param> | ||
/// <returns>True if null, empty, or whitespace, false otherwise.</returns> | ||
public static bool IsMissing(this string value) => string.IsNullOrWhiteSpace(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, this obfuscated what we're doing when calling IsMissing
instead of providing clarity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, gotcha. Was trying to remove some of the boilerplate and focus more on the semantics of what the caller is trying to achieve, but looks like it missed the mark. Will take another look at that, thanks.
GrowthBook/FeatureResult.cs
Outdated
/// <summary> | ||
/// Represents possible values for the source of a feature result. | ||
/// </summary> | ||
public static class SourceIs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend removing since it isn't used.
Also, I think you meant "SourceIDs"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's actually used when calling GetFeatureResult and was intended to be a more fluent-property kind of thing so it reads FeatureResult.SourceIs.DefaultValue or similar. Do you feel that it's more confusing that way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, the more I think about it, we don't currently have much of a fluent-syntax usage in this SDK, would add to the confusion. Renamed.
GrowthBook/GrowthBook.csproj
Outdated
@@ -2,7 +2,7 @@ | |||
|
|||
<PropertyGroup> | |||
<TargetFramework>netstandard2.0</TargetFramework> | |||
<Authors>Ben Whatley</Authors> | |||
<Authors>Ben Whatley,Norhaven</Authors> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would usually only put recognized contributors of the Github repository here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, removed. :)
GrowthBook/GrowthBook.csproj
Outdated
@@ -11,11 +11,17 @@ | |||
<RepositoryUrl>https://github.com/growthbook/growthbook-csharp.git</RepositoryUrl> | |||
<RepositoryType>git</RepositoryType> | |||
<PackageTags>GrowthBook,A/B test,feature toggle</PackageTags> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would recommend to include "flag"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added now.
@Mastermindzh Thanks for the review, updated the PR and had a few questions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't review the test cases due to a lack of time.
I really like the infrastructure for keeping the features up-to-date (with the server side events) 👍 👍. It would be nice to have some usage documentation in the README.md, as any local implementation of this package will probably be impacted because of these changes. Also, I'm not sure how to work with attributes. We are using this package in a web API and for each HTTP request I need to set the attributes in order to target features to a certain audience (user or user role). Could you shed some light on this?, preferably in the form of documentation.
} | ||
} | ||
|
||
public async Task RefreshWith(IDictionary<string, Feature> features, CancellationToken? cancellationToken = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't look like an async function, or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made that call return a Task in the IGrowthBookFeatureCache interface that this implements because, assuming usage is backend in a distributed environment (e.g. cloud), people may want to provide their own cache that works better for their situation, like a Redis-backed one or similar, instead of just an in-memory one. That kind of thing is likely to be more appropriately accessed in an async manner, so I preemptively went that direction.
And you're right, the InMemoryFeatureCache itself has no async needs, but it should probably conform to that pattern anyway. I made a quick change now to remove the 'async' keyword and just explicitly return a Task.CompletedTask to make things a little more clear and avoid the warning, hopefully that all helps. :)
GrowthBook/Experiment.cs
Outdated
@@ -26,7 +26,7 @@ public class Experiment | |||
/// <summary> | |||
/// How to weight traffic between variations. Must add to 1. | |||
/// </summary> | |||
public IList<double> Weights { get; set; } | |||
public List<double> Weights { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd always go for an interface (such as IEnumerable<>
or IList<>
) over a concrete type. That way you're more flexible in your implementation. I also doubt you need a List for this, as these properties come from GrowthBook you probably only need to iterate over this collection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd agree. Hmm... from my perspective, I would tend to prefer an array in here on data objects just so we can both have explicit length checks/indexing available as well as enforce the readonly semantics a little more to discourage adding/removing items, but with the existing SDK implementation using lists I wasn't sure if there was a use case for people wanting to add their own values on the fly to these objects. Taking a breaking change to that might cause problems.
@darklordzw Just wondering, did you have a particular design decision in mind choosing lists over something else for the existing implementation? Might help inform the directions we can go here. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Norhaven, I tend to prefer ILists over arrays in most cases unless there's a reason to go with arrays. I forget why I went with a list over IEnumerable originally, I suspect it was another case of wanting to stay syntactically consistent with the other SDKs, which also use indexable lists.
/// <param name="experiment">The experiment to evaluate.</param> | ||
/// <returns>The experiment result.</returns> | ||
ExperimentResult RunExperiment(Experiment experiment) | ||
public async Task LoadFeatures(GrowthBookRetrievalOptions options = null, CancellationToken? cancellationToken = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// <inheritdoc />
You want documentation on your public methods ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, added. :)
|
||
if (_config.PreferServerSentEvents) | ||
{ | ||
_isServerSentEventsEnabled = response.Headers.TryGetValues("x-sse-support", out var values) && values.Contains("enabled"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw this constant more than once in the code base. Maybe you could make it a constant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, done.
var hostEndpoint = config.ApiHost; | ||
var trimmedHostEndpoint = new string(hostEndpoint?.Reverse().SkipWhile(x => x == '/').Reverse().ToArray()); | ||
|
||
_featuresApiEndpoint = $"{trimmedHostEndpoint}/api/features/{config.ClientKey}"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you'd used a typed http client, you could have contained all the logic related to communicating with the GrowthBook Api. Then this service worker would only contain the logic in keeping the GrowthBookFeatureCache
up to date.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, gotcha. Will look into that, thanks!
GrowthBook/GrowthBook.cs
Outdated
var featureCache = new InMemoryFeatureCache(cacheExpirationInSeconds: 60); | ||
var httpClientFactory = new HttpClientFactory(requestTimeoutInSeconds: 60); | ||
|
||
var loggerFactory = context.DefaultLoggerFactory ?? LoggerFactory.Create(builder => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this a concern of the consumer of this package to configure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think you're right. I'd added in the console/debug logging for testing and provided the DefaultLogLevel property on the Context that could modify this, but looking at it some more I think that's an extraneous property and this can be condensed and not force a particular logging provider on the user. Will fix that. Thanks!
@darklordzw @Mastermindzh @pascal89 Okay, the PR is ready for another look over, please let me know if you have questions. I've left the existing comments open for any further discussion necessary, feel free to resolve them if it works for you. Thanks! |
All my feedback has been implemented and then some. I don't see a reason not to merge this 😄. |
@Norhaven Great work! Still have a little remark though. It would be nice if you could include two additions:
|
@pascal89 Just updated the changelog now, thanks! For the readme and documentation, I also have an extensive ASP.Net Core backend example demonstrating the basic use cases and how to set things up with this version of the SDK that will go into the https://github.com/growthbook/examples repo once this PR is complete and out to NuGet. I'm thinking that once the example PR is approved and merged over there then I can link it in the readme over here, would that be sufficient or do you think that it would still help to put some basic 'getting started' info here as well? |
I'm glad you've worked on some examples as well 😄 As long as we'll link to the example(s) from the readme it's fine for me. If you want my opinion on the examples you'll be adding, feel free to request me as a reviewer. I think that accounts for @Mastermindzh as well. |
PR has been merged into main. Thanks again for your contributions @Norhaven! I'll have a version up on NuGet in a few. |
v1.0.0 has is live on NuGet now. |
This implements the C# SDK to be compliant with the 0.5.2 version of the GrowthBook SDK spec. Things that are a part of this include:
Please let me know if you have any questions.
Thanks!