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

[FEATURE] merging EvaluationContext with Context #282

Closed
kevinschoonover opened this issue Aug 30, 2024 · 3 comments
Closed

[FEATURE] merging EvaluationContext with Context #282

kevinschoonover opened this issue Aug 30, 2024 · 3 comments
Labels
enhancement New feature or request Needs Triage

Comments

@kevinschoonover
Copy link
Contributor

kevinschoonover commented Aug 30, 2024

Requirements

Apologies if this has already been answered, I tried looking around the issues for a bit but wasn't able to find an answer as to 'why ctx and evalCtx are different from the API POV?'

The function signature is pretty explicit about 'don't get confused'
image

, but I was wondering if the evalCtx just be stuffed inside of the regular context using context.WithValue and then extracted accordingly? Consider the following hypothetical web api psuedo code which authenticates the user and adds the authenticated user's information into the context (excuse the technical inaccuracies as I am not a gin user, but I see it used in the documentation)

package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

const defaultMessage = "Hello!"

func AuthenticationMiddleware() gin.HandlerFunc {
  // authenticate the user
  
  return func(c *gin.Context) {
    // authenticate the user
    authInfo := GetAuthInfo()
    // set the current user's information in the context
    c.Ctx = context.WithValue(userKey{}, authInfo)
    c.Next()
  }
}

func main() {
    // Initialize Go Gin
    engine := gin.Default()
    
    engine.Use(AuthenticationMiddleware())
    // Setup a simple endpoint
    engine.GET("/hello", func(c *gin.Context) {
       // we can now tell who is using this endpoint
       authInfo := context.Value(userKey{})
        c.JSON(http.StatusOK, defaultMessage)
    })
   engine.GET("/hello2", func(c *gin.Context) {
       // we can now tell who is using this endpoint
       authInfo := context.Value(userKey{})
        c.JSON(http.StatusOK, defaultMessage)
    })

    engine.Run()
}

For every route that wants to take advantage of the evalCtx they then have to extra the information in their individual routes like so

    engine.GET("/hello", func(c *gin.Context) {
        authInfo := context.Value(userKey{})
        client.Boolean(c.Ctx(), "flag1", false, ConstructEvalContext(authInfo))
        c.JSON(http.StatusOK, defaultMessage)
    })
   engine.GET("/hello2", func(c *gin.Context) { 
        authInfo := context.Value(userKey{})
        client.Boolean(c.Ctx(), "flag1", false, ConstructEvalContext(authInfo))
        c.JSON(http.StatusOK, defaultMessage)
    })

where ConstructEvalContext is some magical function that pulls the authenticate information and creates the appropriate evalCtx.

The API I was hoping to use was create another middleware which does this on behalf each of the routes

package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

const defaultMessage = "Hello!"

func AuthenticationMiddleware() gin.HandlerFunc {
  // authenticate the user
  return func(c *gin.Context) {
    c.Ctx =  context.WithValue(userKey{}, authenticationInformation)
    c.Next()
  }
}

func EvaluationContextMiddleware()  gin.HandlerFunc {
  return func(c *gin.Context) {
   // grab the authentication information from the user
   authInfo = context.Value(userKey{}) 
  // set the context that is passed to the handlers
   c.Ctx = context.WithValue(evaluationCtxKey{}, ConstructEvalContext(authInfo))
    // or even better
   c.Ctx = openFeature.NewEvaluationContext(c.Ctx(), ConstructEvalContext(authInfo))
    c.Next()
  }
}

func main() {
    // Initialize Go Gin
    engine := gin.Default()
    
    engine.Use(AuthenticationMiddleware())
    engine.Use(EvaluationContextMiddleware())
    // Setup a simple endpoint
    engine.GET("/hello", func(c *gin.Context) {
       client.Boolean(c.Ctx(), "flag1", false)
        c.JSON(http.StatusOK, defaultMessage)
    })
   engine.GET("/hello2", func(c *gin.Context) {
        client.Boolean(c.Ctx(), "flag1", false)
        c.JSON(http.StatusOK, defaultMessage)
    })

    engine.Run()
}

; however, these two concepts being separate make the existing API have to be handled individually in every place we want to reference the evaluation context. This would also allow this to be passed transparently through to internal libraries that want to take advantage of evaluation contexts. I can write a helper function to do this for my application, but this seems like a common pattern APIs will want to use.

The concerns I see with this approach are:

  1. what if multiple people set this key in the context - but the existing openFeature.NewEvaluationContext api can hide these concerns from the user.
  2. why can't we use the existing openfeature.SetEvaluationContext or client.SetEvaluationContext - this value would change per request so for openfeature it wouldn't be thread safe. For client, I would have to create a new client for every request and pass it through the context making it the same behavior that is shown above
@kevinschoonover kevinschoonover added enhancement New feature or request Needs Triage labels Aug 30, 2024
@beeme1mr
Copy link
Member

Hey @kevinschoonover, I think what you're looking for is transaction-scoped evaluation context. This is defined in the spec but not implemented in Go yet.

@kevinschoonover
Copy link
Contributor Author

That is definitely what I am looking for. Apologies for the noise!

@beeme1mr
Copy link
Member

No problem! The issue to implement it is up for grabs if you're interested 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request Needs Triage
Projects
None yet
Development

No branches or pull requests

2 participants