Skip to content
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

[RFC] feat: add support for strongly typed exp/gates #1

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

daniel-statsig
Copy link
Contributor

@daniel-statsig daniel-statsig commented Oct 22, 2024

Update - Oct 28

2d36d5d Added support for Feature Gate memoization. You can now specify that a gate should be memoized, as well as what UnitID to memoize on (Defaults to userID).

class SdkDemoGates {
    static let aGate = TypedGateName("a_gate", isMemoizable: true)
    static let partialGate = TypedGateName(
        "partial_gate",
        isMemoizable: true,
        memoUnitIdType: "stableID"
    )
}

e25676d Added default memoized experiment types for UserID (TypedExperimentMemoizedByUserID) and StableID (TypedExperimentMemoizedByStableID). This removes the need to specify is isMemoizable and memoUnitIdType

struct SdkDemoTypedAnExperiment: TypedExperimentMemoizedByUserID {
    static var name = "an_experiment"

    var groupName: SdkDemoSimpleGroupName?
    var value: TypedNoValue?
}

Note: SdkDemoSimpleGroupName is an enum that only has two groups. It can be shared among many experiments since this is a common setup pattern.

public enum SdkDemoSimpleGroupName: String, TypedGroupName {
    case control = "Control"
    case test = "Test"
}

With this change, a developer can write their own type definitions for Statsig.

  • Here is an example of a type definitions file for an Sdk Demo project.
  • Here is an example of the strict types being used.

Usage

Gates

A type file is defined by the implementor.

@objc class SdkDemoGates: NSObject {
    @objc static let aGate = TypedGateName("a_gate")
    @objc static let partialGate = TypedGateName("partial_gate")
}

Then the gates can be checked using Statsig.typed:

let gate = Statsig.shared.typed.getFeatureGate(SdkDemoGates.aGate, user)
if gate.value {
    // Do Something
}

if Statsig.shared.typed.checkGate(SdkDemoGates.aGate, user) {
    // Do Something    
}

Experiments

A type file is defined by the implementor.

class SdkDemoExperiments {
    static let AnExperiment = SdkDemoTypedAnExperiment.self
    static let AnotherExperiment = SdkDemoTypedAnotherExperiment.self
}

struct SdkDemoTypedAnExperiment: TypedExperiment {
    static var name = "an_experiment"
    static var isMemoizable = true
    static var memoUnitIdType = "userID"
    
    var groupName: SdkDemoTypedAnExperimentGroup?
    enum SdkDemoTypedAnExperimentGroup: String, TypedGroupName {
        case control = "Control"
        case test = "Test"
    }
    
    var value: TypedNoValue?
}

Then the experiment can be checked using Statsig.typed:

let anExperiment = Statsig.shared.typed.getExperiment(SdkDemoExperiments.AnExperiment, user)
switch anExperiment.groupName {
case .test:
    texts.append("\(anExperiment.name): Test Group")
    break
    
case .none:
    fallthrough
case .control:
    texts.append("\(anExperiment.name): Control Group")
    break
}

Strictly Typed Experiment Values

Its also possible to define the value object to be of a given type by adding a decodable type to the definition

struct SdkDemoTypedAnotherExperiment: TypedExperiment {
    static var name = "another_experiment"
    
    var groupName: SdkDemoTypedAnotherExperimentGroup?
    enum SdkDemoTypedAnotherExperimentGroup: String, TypedGroupName {
        case control = "Control"
        case testOne = "Test One"
        case testTwo = "Test Two"
    }
    
    var value: AnotherExperimentValue? // <---- Value must now be AnotherExperimentValue
    struct AnotherExperimentValue: Decodable {
        let aString: String
        let aBool: Bool

        enum CodingKeys: String, CodingKey {
            case aString = "a_string"
            case aBool = "a_bool"
        }
    }
}

Then you can use the value with the strict typing:

let anotherExperiment = Statsig.shared.typed.getExperiment(SdkDemoExperiments.AnotherExperiment, user)
      
if anotherExperiment.value?.aBool === true {
    // Do Something
}

Experiment Memoization (Freezing/Locking)

By setting the isMemoizable field in your TypedExperiment to return true. Statsig typed will only evaluate the experiment once, repeated calls to the same experiment will return the same result.

If you would like the memoization to break when a specific ID type is changed in the StatsigUser, you can set the memoUnitIdType field in your TypedExperiment.

For example, if you set the memoUnitIdType to 'userID':

struct SdkDemoTypedAnExperiment: TypedExperiment {
    static var name = "an_experiment"
    static var isMemoizable = true
    static var memoUnitIdType = "userID" // <- Set ID Type
 
    // ...
}


let statsig = Statsig.shared
statsig.setGlobalUser(StatsigUser(userID: "a-user"))

let exp1 = statsig.typed.getExperiment(SdkDemoExperiments.AnExperiment)
let exp2 = statsig.typed.getExperiment(SdkDemoExperiments.AnExperiment) // will be the same as exp2

statsig.setGlobalUser(StatsigUser(userID: "b-user"))

let exp3 = statsig.typed.getExperiment(SdkDemoExperiments.AnExperiment) // will be re-evaluated against "b-user"

@@ -0,0 +1,59 @@
import Foundation
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: manually written by the implementor, but could be code generated by Statsig

setupConstraints(for: labels)
}

private func evalAndAppendResults(texts: inout [String]) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is where the actual strict types are used

@daniel-statsig daniel-statsig changed the title feat: add support for strongly typed exp/gates [RFC] feat: add support for strongly typed exp/gates Oct 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant