@@ -20,13 +20,17 @@ import type {
2020 UserContext ,
2121} from "./types" ;
2222
23+ /** Storage key for anonymous ID (shared with tracker) */
24+ const ANONYMOUS_ID_KEY = "did" ;
25+
2326/**
2427 * Core flags manager - lightweight API client with best practices:
2528 * - Stale-while-revalidate caching
2629 * - Request batching (multiple flags → single request)
2730 * - Request deduplication
2831 * - Visibility API awareness (pause when hidden)
2932 * - Optimistic storage hydration
33+ * - Automatic anonymous ID for gradual rollouts
3034 */
3135export class CoreFlagsManager implements FlagsManager {
3236 private config : FlagsConfig ;
@@ -70,11 +74,54 @@ export class CoreFlagsManager implements FlagsManager {
7074 this . initialize ( ) ;
7175 }
7276
77+ /**
78+ * Get or create anonymous ID for deterministic rollouts.
79+ * Uses the same storage key as the tracker ("did") for consistency.
80+ */
81+ private getOrCreateAnonymousId ( ) : string | null {
82+ if ( typeof localStorage === "undefined" ) {
83+ return null ;
84+ }
85+
86+ try {
87+ let id = localStorage . getItem ( ANONYMOUS_ID_KEY ) ;
88+ if ( id ) {
89+ return id ;
90+ }
91+
92+ id = `anon_${ crypto . randomUUID ( ) } ` ;
93+ localStorage . setItem ( ANONYMOUS_ID_KEY , id ) ;
94+ return id ;
95+ } catch {
96+ // localStorage might be blocked (private browsing, etc.)
97+ return null ;
98+ }
99+ }
100+
101+ /**
102+ * Ensure user context has an identifier for deterministic flag evaluation.
103+ * If no userId/email provided, inject anonymous ID as userId.
104+ */
105+ private ensureUserIdentity ( user ?: UserContext ) : UserContext | undefined {
106+ // If user already has identification, use as-is
107+ if ( user ?. userId || user ?. email ) {
108+ return user ;
109+ }
110+
111+ const anonymousId = this . getOrCreateAnonymousId ( ) ;
112+ if ( ! anonymousId ) {
113+ return user ;
114+ }
115+
116+ // Inject anonymous ID as userId for deterministic rollouts
117+ return { ...user , userId : anonymousId } ;
118+ }
119+
73120 private withDefaults ( config : FlagsConfig ) : FlagsConfig {
74121 return {
75122 clientId : config . clientId ,
76123 apiUrl : config . apiUrl ?? "https://api.databuddy.cc" ,
77- user : config . user ,
124+ user : this . ensureUserIdentity ( config . user ) ,
78125 disabled : config . disabled ?? false ,
79126 debug : config . debug ?? false ,
80127 skipStorage : config . skipStorage ?? false ,
@@ -420,7 +467,7 @@ export class CoreFlagsManager implements FlagsManager {
420467 * Update user context and refresh flags
421468 */
422469 updateUser ( user : UserContext ) : void {
423- this . config = { ...this . config , user } ;
470+ this . config = { ...this . config , user : this . ensureUserIdentity ( user ) } ;
424471
425472 // Recreate batcher with new user params
426473 this . batcher ?. destroy ( ) ;
0 commit comments