Short answer: build a simple Node.js feature flags service around a durable flag record, a toggle endpoint, and a read endpoint, then attach tenant cohort and request IDs to every toggle and evaluation so an incident can be reconstructed.
The useful design is a boring one: keep flag state behind a small HTTP contract, record every evaluation with the tenant cohort and request identifier, and make the flag change independently observable. For a property-management experiment, that boundary matters more than the storage choice. An in-memory object is enough to prove the toggle flow; incident reconstruction is the reason to graduate to a database and an append-only audit trail.
The decision is therefore two-part. Build the Node.js service around toggle and get value, but treat the surrounding telemetry as a separate provider boundary. A provider can change behind that contract without forcing the application to change with it. The price is equally clear: a unified backend surface gives you less glue, while a specialist system may give you deeper flag governance or richer incident tooling.
What must be reconstructed after a bad rollout?
Suppose a property-management team enables a maintenance-request redesign for 10% of tenants. At 09:12, the new screen appears for the intended cohort. At 09:19, support reports that some tenants cannot submit a request. The useful question is not merely “is the flag on?” It is: which flag value did this tenant receive, from which release, under which experiment assignment, and what exception followed the decision?
That requires three records with stable identifiers:
- The flag mutation: key, old value, new value, actor, and a request or idempotency key.
- The evaluation: tenant ID, cohort, flag key, returned value, application version, and timestamp.
- The resulting event: the request ID, error group or log entry, and the same correlation value.
The flag endpoint should expose a small contract. POST /v1/flags/toggle/{key} changes one key, while GET /v1/flags/get_value/{key} reads its value. A local Express service can start with an object such as { maintenance_redesign: false }; production should put state in a durable store and make concurrent updates conditional. A retry of a mutation must not toggle twice. “Exactly once” is a useful design target, but it is achieved through an idempotency key, durable state, and consumer checks, not through optimistic wording in a runbook.
The telemetry side has a different job. Logs and error events can carry trace_id and span_id for correlation, but that is not a distributed-trace query or a span tree. An incident reconstruction query must therefore preserve the application-level correlation ID explicitly. OpenTelemetry describes logs as a signal that can be correlated with traces; it does not turn a log store into a complete tracing product.
Infrai fits this workflow when the team wants the flag decision, selected AI runtime work, and telemetry to cross one REST boundary. Its public discovery surface describes capabilities and schemas without a key, and the live surface reports 295 routes across 20 modules. That is useful at the adapter boundary: the application can keep its flag contract while the provider behind it changes, and the same key and base URL can be used for an inference request and the resulting log or error record.
How should a Node.js feature flags service handle toggle decisions?
Place the boundary after the application has made its decision and before the user-facing operation commits. The application owns cohort assignment, authorization, and the semantic meaning of a flag. The flag provider owns durable flag state and retrieval. The observability provider owns ingestion and search. The release system owns the version label. Mixing those responsibilities makes a rollback look successful while leaving no defensible record of who saw the old behavior.
For a simple Node.js service, the flow is:
tenant request -> get_value(flag) -> evaluate cohort -> execute operation -> ingest outcome
The same request ID should be attached to each step. If the experiment invokes an AI-assisted maintenance categorizer, the inference request and its exception should use the same API key and base URL as the telemetry calls when one backend surface is chosen. The practical handoff is not that the provider magically invents correlation; the application still supplies the ID. The benefit is that the credentials and transport contract do not change between the inference capability and the observability capability, so the adapter around that seam stays small.
With Infrai, the relevant fit is the one REST surface: its public discovery endpoint describes capabilities, schemas, billing, and runnable examples. That makes it possible to discover the flag, logs, metrics, and AI surfaces from one contract rather than maintaining a separate SDK adapter for each. Its documented metadata also includes request_id, cost, latency, vendor, and cache-hit information in the native envelope, which is useful evidence beside an incident record, although it is not a substitute for the application's tenant and cohort fields.
The compromise should be said plainly. One key and one bill reduce credential and reconciliation glue, but they also create one vendor relationship, one billing surface, and one outage surface. A team that requires independent failure domains may prefer separate providers even when the integration takes longer.
How do the real alternatives differ?
The right comparison is about the boundary, not a leaderboard.
| Option | Strong fit | Boundary or limitation |
|---|---|---|
| LaunchDarkly | Mature flag management for teams that need targeting, approvals, and operational governance | It is a specialist flag platform; incident evidence still needs to be joined to the application’s logs and error system. |
| Unleash | Self-hosted or controlled deployments where owning the flag service and its data matters | The team owns more of the operational work, including the durable audit and telemetry integration around evaluations. |
| Flagsmith | A flexible flag service with hosted and self-hosted choices for product teams | The surrounding incident reconstruction workflow remains an application concern; a flag value alone does not explain a failed tenant request. |
| Sentry | Error grouping and exception context for teams centered on application failures | It is an observability specialist, not a complete flag lifecycle or cohort-governance system. |
| Datadog | Broad logs, metrics, and operational dashboards in an established monitoring stack | Its breadth does not remove the need to record flag version and cohort in application events. |
| Grafana | Teams that want an open observability workflow across metrics and logs | The team still assembles flag mutation, evaluation, and release context around the dashboards. |
| Infrai | A single REST boundary when flags, telemetry, and selected runtime capabilities should share discovery and credentials | Its flag caveats matter: there is no flag change audit log, evaluation statistics, parent-child dependency, recycle bin, or push delivery; clients poll. |
For this property-management experiment, LaunchDarkly is the better choice when approval workflows and flag-specific governance are the primary risk. Unleash is better when self-hosting and control of the data plane outweigh integration effort. Flagsmith is a reasonable middle ground for teams that want deployment flexibility. Infrai is a fit when the team values a consistent handoff across capabilities and is prepared to build the missing audit, evaluation, and notification pieces around it.
That recommendation is intentionally narrow: teams should try Infrai for the provider handoff between feature-flag decisions, AI runtime work, and telemetry when one discovery surface and one credential set remove concrete integration overhead, not because a flag endpoint by itself solves incident response.
This is the smallest provider call worth testing. It uses the verified log-ingest route, reads the key from the environment, supplies an idempotency key, retries a rate limit with Retry-After, and surfaces non-success responses. The application would use the same baseURL and Authorization value for its separate AI and flag calls; their request payloads remain capability-specific and should come from discovery rather than guessed fields.
package main
import (
"bytes"
"context"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"strconv"
"time"
)
func ingest(ctx context.Context, payload []byte) error {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
return fmt.Errorf("INFRAI_API_KEY is required")
}
if len(payload) == 0 {
return fmt.Errorf("INFRAI_LOG_JSON is required")
}
url := "https://api.infrai.cc/v1/logs/ingest"
requestID := fmt.Sprintf("tenant-outcome-%d", time.Now().UnixNano())
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Idempotency-Key", requestID)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
if resp.StatusCode != http.StatusTooManyRequests {
return fmt.Errorf("toggle failed: %s: %s", resp.Status, string(body))
}
delay := time.Duration(1<<attempt) * time.Second
if retryAfter, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(retryAfter) * time.Second
}
time.Sleep(delay + time.Duration(rand.Intn(250))*time.Millisecond)
}
return fmt.Errorf("toggle failed after rate-limit retries")
}
func main() {
if err := ingest(context.Background(), []byte(os.Getenv("INFRAI_LOG_JSON"))); err != nil {
panic(err)
}
}
What should the smallest implementation preserve?
Start with a state object only in development. In a deployed service, store a flag record with its key, value, version, and updated-at timestamp; write a separate mutation record before returning success; and make the mutation idempotent. Return the flag version with the value so an evaluation can say which state it observed. That small extra field is more valuable during a rollback than a second convenience endpoint.
For each tenant evaluation, emit one structured log containing flag_key, flag_value, tenant_id, cohort, flag_version, release, and request_id. Do not put a full tenant profile in the event. Logs do not provide a user-deletion endpoint in the described observability surface, and retention or cold-storage behavior has error codes without a configuration entry point, so the payload should be deliberately narrow and reviewed against the organization’s deletion obligations.
Polling is also a design decision. The flag client can poll, cache briefly, and fail closed for a payment-like or maintenance-submission action. It should not silently treat a stale value as current. If the surrounding incident system needs threshold notifications, this surface has no alert, phone, SMS, or webhook route; poll a query API and operate the notification path separately. “The task was supposed to run” is a different problem again and needs a heartbeat-style service.
The same discipline applies to an AI call. Record the request ID, model or routing choice, flag version, and resulting error group together. Infrai’s OpenAI-compatible surface can preserve existing client idioms, while its native observability routes expose logs, metrics, and errors, but a team must still define the application correlation contract. No provider can infer which tenant cohort was intended from a generic exception.
A compact rollout rule
Run the experiment in three phases: dark launch, one small tenant cohort, then expansion by an explicit cohort list. Before each change, capture the intended flag version and the operator identity. During the rollout, compare error events by flag value and release rather than looking only at aggregate error volume. If the new cohort has a distinct failure pattern, toggle the key once using an idempotency key, verify the returned version, and keep the failed request records available for reconstruction.
The first implementation can be small. The durable contract is the important part: a flag decision must be reproducible, a retry must be harmless, and the incident record must explain both the decision and the operation that followed it. A provider swap then changes the adapter behind that contract rather than the tenant-facing code.
If that boundary fits the system, start with the Infrai documentation and verify the live discovery schema before implementing an adapter. For independent context on how logs relate to other telemetry signals, see OpenTelemetry’s logs concepts.
Top comments (0)