A Go client for Flaggr, an OpenFeature-compatible feature flag platform.
The client evaluates flags through the Flaggr REST API. It has typed methods for boolean, string, number and object flags. It can also answer boolean and string flags in-process from a cache that you load.
go get github.com/flaggr-dev/flaggr-go@latestThe module needs Go 1.27 or later and uses only the standard library. The package name is flaggr.
package main
import (
"context"
"log"
"os"
flaggr "github.com/flaggr-dev/flaggr-go"
)
func main() {
client, err := flaggr.NewClient(
flaggr.WithAPIKey(os.Getenv("FLAGGR_API_KEY")), // project API token (fgr_...)
flaggr.WithServiceID(os.Getenv("FLAGGR_SERVICE_ID")), // service ID from the Flaggr console
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
enabled, err := client.Boolean(context.Background(), "checkout-v2", false, flaggr.EvalContext{
TargetingKey: "user-123",
})
if err != nil {
log.Printf("checkout-v2: using the default: %v", err)
}
log.Printf("checkout-v2 = %v", enabled)
}The client needs two values from your Flaggr project:
- An API token. Use a project API token (
fgr_...). A token with read permission can evaluate flags. Create one in the project settings of the Flaggr console and keep it out of source control. Personal access tokens (fgp_...) can't evaluate flags. See API Tokens. - A service ID. This is the ID of the service whose flags you evaluate. Copy it from the service's Connection tab in the console. The service's slug or display name won't work.
Every request sends the token in an Authorization: Bearer <token> header.
| Option | Default | Description |
|---|---|---|
WithAPIKey(token) |
required | Project API token |
WithServiceID(id) |
required | Service ID |
WithEnvironment(env) |
"production" |
The environment to evaluate in, such as development or staging |
WithAPIURL(url) |
"https://flaggr.dev" |
Base URL of the Flaggr API. https://api.flaggr.dev is the data plane (see Rate limits) |
WithHTTPClient(hc) |
&http.Client{Timeout: 5 * time.Second} |
Your own *http.Client, for timeouts, proxies or a custom transport. nil keeps the default |
WithLocalEvaluation(true) |
false |
Answers Boolean and String from the local cache (see Local evaluation) |
The options remove surrounding whitespace from the API key, service ID, environment and URL, so a value read from an environment variable or a secrets file with a trailing newline still works. They also remove trailing slashes from the URL.
NewClient returns an error when the API key or the service ID is empty. Each flag is configured separately in each environment, so a key can return different values in development, staging and production. See Environments.
Each evaluation sends one POST /api/flags/evaluate request.
| Method | Returns |
|---|---|
Boolean(ctx, key, defaultValue, evalCtx...) |
(bool, error) |
String(ctx, key, defaultValue, evalCtx...) |
(string, error) |
Number(ctx, key, defaultValue, evalCtx...) |
(float64, error) |
Object(ctx, key, defaultValue, evalCtx...) |
(map[string]interface{}, error) |
Resolve(ctx, key, defaultValue, evalCtx...) |
(*EvaluationDetail, error) with the value, reason, variant and any error code |
The evaluation context is optional. If you pass more than one, only the first is used.
ctx := context.Background()
user := flaggr.EvalContext{
TargetingKey: "user-456",
Attributes: map[string]interface{}{
"plan": "enterprise",
"country": "AU",
"seats": 25,
},
}
theme, err := client.String(ctx, "theme", "light", user)
limit, err := client.Number(ctx, "rate-limit", 100, user)
banner, err := client.Object(ctx, "banner-config", map[string]interface{}{"text": ""}, user)
detail, err := client.Resolve(ctx, "checkout-v2", false, user)
if err == nil {
log.Printf("value=%v reason=%s variant=%s", detail.Value, detail.Reason, detail.Variant)
}TargetingKey is usually the user ID. Overrides and percentage rollouts are based on it. Attributes hold the values that your targeting rules match on. Attribute values must be strings, numbers or booleans: https://flaggr.dev returns 400 Bad Request for nested objects, arrays and nil. See Targeting Rules.
Outside local evaluation, each call to Boolean, String, Number, Object or Resolve is one HTTPS request, and the client doesn't cache answers. The default base URL, https://flaggr.dev, allows 1,000 requests a minute from one IP address, 5,000 per token and 10,000 per service. Past that it answers 429 Too Many Requests, and the typed methods return your default with an error. See Rate Limiting.
In a busy service, evaluate each flag once per request or job and pass the value along. You can also send evaluations to the data plane with flaggr.WithAPIURL("https://api.flaggr.dev"), which the TypeScript SDK uses by default. The data plane doesn't assign experiments yet, so a flag with a running experiment can return a different variant there than on https://flaggr.dev.
The evaluation methods don't panic. A request can fail because of a network error, a timeout or a status other than 200, such as 401, 403 or 429. When it does, the typed methods return your default value and a non-nil error, and Resolve returns a nil detail and the error. The error names the status and quotes up to 512 bytes of the response body. Log the error and use the default:
enabled, err := client.Boolean(ctx, "checkout-v2", false, user)
if err != nil {
log.Printf("checkout-v2: using the default: %v", err)
}In two cases the typed methods return your default value with a nil error:
- The flag doesn't exist in that service and environment. Call
Resolveto see the reason:NOT_FOUNDfromhttps://flaggr.dev. - The flag's value is of a different type from the method you called, such as
Numberon a string flag. One exception:Booleanreads the string"true"astrueand any other string asfalse.
Flaggr gives the same 401 or 403 for a service ID that doesn't exist or that the token can't read. If you get one, check the service ID as well as the token.
With WithLocalEvaluation(true), Boolean and String answer from an in-process cache without a network call. You fill the cache yourself because the client doesn't download flag definitions. Number, Object and Resolve still call the API.
client, err := flaggr.NewClient(
flaggr.WithAPIKey(os.Getenv("FLAGGR_API_KEY")),
flaggr.WithServiceID(os.Getenv("FLAGGR_SERVICE_ID")),
flaggr.WithLocalEvaluation(true),
)
if err != nil {
log.Fatal(err)
}
client.LoadFlags([]flaggr.CachedFlag{{
Key: "dark-mode",
Enabled: true,
DefaultValue: false,
Overrides: []flaggr.CachedOverride{
{Identifiers: []string{"user-123"}, Value: true},
},
}})
on, _ := client.Boolean(ctx, "dark-mode", false, flaggr.EvalContext{TargetingKey: "user-123"}) // trueLocal evaluation works like this:
- A key that isn't in the cache returns your default.
- A disabled flag returns its
DefaultValue. - An enabled flag returns the first override whose
Identifiersinclude theTargetingKey. If no override matches, it returns the flag'sDefaultValue. - Cached values of the wrong type are skipped.
Targeting rules and percentage rollouts are evaluated only by the API. SetFlag adds or replaces one flag, InvalidateFlag removes one, and LoadFlags replaces the whole cache. A Client is safe for concurrent use.
The errorreport package sends your program's errors and panics to Flaggr. The console's Issues page groups them into issues and shows which flag variants each issue happens under. It needs a project API token (fgr_...), because Flaggr refuses SDK keys for error reports.
import "github.com/flaggr-dev/flaggr-go/errorreport"
func main() {
reporter := errorreport.New(errorreport.Options{
Token: os.Getenv("FLAGGR_ERRORS_TOKEN"), // the default when Token is empty
Release: "checkout-api@2.8.1",
Environment: "production",
})
defer reporter.Close() // sends what is queued
defer reporter.Recover() // reports a panic, then re-panics
http.Handle("/", reporter.Middleware(mux)) // reports handler panics with the method and path
if err := placeOrder(ctx); err != nil {
reporter.CaptureError(err,
errorreport.WithTags(map[string]string{"step": "pay"}),
errorreport.WithFlags(map[string]string{"coupon-engine": variant}),
errorreport.WithUser(userID),
)
}
}What the package does:
- Grouping. Flaggr groups events by the innermost frames of your own code, without line numbers, so a deploy doesn't split an issue. Use
WithFingerprintto choose the group yourself. - Flags.
SetFlagrecords a flag's variant for every later event, andWithFlagssets the variants on one event. - Context.
SetTag,SetUserandAddBreadcrumbadd to the events that follow. - Sending. Events are sent in the background in batches. Messages and stacks are cut to 2,000 characters. At most 60 events are sent a minute.
- Bad tokens. If Flaggr refuses the token, the reporter stops sending and tells you through
Options.Logf. - Opting out.
Options.Disabledturns the reporter into a no-op.
Set TracesSampleRate and the middleware also records requests as spans for Flaggr's Performance page. That page shows throughput, p50/p95/p99 latency and failures per route, plus each trace's waterfall:
reporter := errorreport.New(errorreport.Options{Token: token, Release: "checkout-api@2.8.1", TracesSampleRate: 0.1})
mux.HandleFunc("GET /orders/{id}", getOrder) // spans are named "GET /orders/{id}"
http.ListenAndServe(":8080", reporter.Middleware(mux))
func getOrder(w http.ResponseWriter, r *http.Request) {
span, ctx := reporter.StartSpan(r.Context(), "load order", "db.query") // a child span
defer span.End()
client := &http.Client{Transport: reporter.Transport(nil)} // sends traceparent downstream
req, _ := http.NewRequestWithContext(ctx, "GET", pricingURL, nil)
_, _ = client.Do(req)
}How tracing behaves:
- Propagation. The middleware continues an incoming W3C
traceparentheader. It follows a caller that sampled the trace; for a caller that didn't (such as a browser that only passes the ids along), it samples the trace at its ownTracesSampleRateand keeps the trace id.Transportpassestraceparenton to the services you call, and records each call as anhttp.clientspan. - Linking errors to traces. Errors captured with
WithRequest(r)orWithContext(ctx)carry the trace id, so the issue links to its trace. This works even when the trace isn't sampled. - Joining the browser. An
X-Request-Idrequest header is recorded on the span, and Flaggr uses it to find the page load that made the request.
Flaggr also serves the OpenFeature Remote Evaluation Protocol (OFREP). To use the OpenFeature Go SDK instead of this client, use its OFREP provider:
go get github.com/open-feature/go-sdk github.com/open-feature/go-sdk-contrib/providers/ofrepprovider := ofrep.NewProvider("https://api.flaggr.dev/api",
ofrep.WithBearerToken(os.Getenv("FLAGGR_API_KEY")),
ofrep.WithHeader("X-Service-Id", os.Getenv("FLAGGR_SERVICE_ID")),
ofrep.WithHeader("X-Environment", "production"),
)
if err := openfeature.SetProviderAndWait(provider); err != nil {
log.Fatal(err)
}
client := openfeature.NewClient("my-service")
enabled, err := client.BooleanValue(ctx, "checkout-v2", false, openfeature.NewEvaluationContext("user-123", nil))The provider adds /ofrep/v1/evaluate/flags/{key} to the base URL itself. Set the X-Environment header: OFREP evaluates in development by default, while this client defaults to production. https://api.flaggr.dev/api is the data plane, which doesn't assign experiments yet. To get the same experiment assignments as this client's default, use https://flaggr.dev/api as the base URL.
To report a vulnerability, see SECURITY.md.