@databuddy/sdk 2.3.1 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,6 @@
1
- import { D as DatabuddyConfig } from '../shared/@databuddy/sdk.CeYE_kaj.js';
2
- import * as react from 'react';
3
- import { ReactNode } from 'react';
4
- import * as jotai_vanilla_internals from 'jotai/vanilla/internals';
5
- import { d as FlagsConfig, c as FlagState } from '../shared/@databuddy/sdk.OK9Nbqlf.js';
6
- export { b as FlagResult, e as FlagsContext } from '../shared/@databuddy/sdk.OK9Nbqlf.js';
1
+ import { D as DatabuddyConfig } from '../shared/@databuddy/sdk.C9b3SVYK.js';
2
+ import React, { ReactNode } from 'react';
3
+ import { F as FlagsConfig, a as FeatureState, b as FlagState, c as FlagsContext } from '../shared/@databuddy/sdk.B6nwxnPC.js';
7
4
 
8
5
  /**
9
6
  * React/Next.js component that injects the Databuddy tracking script.
@@ -56,18 +53,61 @@ export { b as FlagResult, e as FlagsContext } from '../shared/@databuddy/sdk.OK9
56
53
  */
57
54
  declare function Databuddy(props: DatabuddyConfig): null;
58
55
 
56
+ /** biome-ignore-all lint/correctness/noUnusedImports: we need to import React to use the createContext function */
57
+
59
58
  interface FlagsProviderProps extends FlagsConfig {
60
59
  children: ReactNode;
61
60
  }
62
- declare function FlagsProvider({ children, ...config }: FlagsProviderProps): react.FunctionComponentElement<{
63
- children?: ReactNode;
64
- store?: jotai_vanilla_internals.INTERNAL_Store;
65
- }>;
66
- declare function useFlags(): {
67
- isEnabled: (key: string) => FlagState;
68
- fetchAllFlags: () => Promise<void> | undefined;
69
- updateUser: (user: FlagsConfig["user"]) => void;
70
- refresh: (forceClear?: boolean) => void;
71
- };
61
+ /**
62
+ * Flags provider component
63
+ * Creates a manager instance and provides flag methods to children
64
+ */
65
+ declare function FlagsProvider({ children, ...config }: FlagsProviderProps): React.JSX.Element;
66
+ /**
67
+ * Access the full flags context
68
+ * @example
69
+ * const { isOn, getFlag, refresh } = useFlags();
70
+ */
71
+ declare function useFlags(): FlagsContext;
72
+ /**
73
+ * Get a flag's full state with loading/error handling
74
+ * @example
75
+ * const flag = useFlag("my-feature");
76
+ * if (flag.loading) return <Skeleton />;
77
+ * return flag.on ? <NewFeature /> : <OldFeature />;
78
+ */
79
+ declare function useFlag(key: string): FlagState;
80
+ /**
81
+ * Simple feature check - returns { on, loading, value, variant }
82
+ * @example
83
+ * const { on, loading } = useFeature("dark-mode");
84
+ * if (loading) return <Skeleton />;
85
+ * return on ? <DarkTheme /> : <LightTheme />;
86
+ */
87
+ declare function useFeature(key: string): FeatureState;
88
+ /**
89
+ * Boolean-only feature check with default value
90
+ * Useful for SSR-safe rendering where you need a boolean immediately
91
+ * @example
92
+ * const isDarkMode = useFeatureOn("dark-mode", false);
93
+ * return isDarkMode ? <DarkTheme /> : <LightTheme />;
94
+ */
95
+ declare function useFeatureOn(key: string, defaultValue?: boolean): boolean;
96
+ /**
97
+ * Get a flag's typed value
98
+ * @example
99
+ * const maxItems = useFlagValue("max-items", 10);
100
+ * const theme = useFlagValue<"light" | "dark">("theme", "light");
101
+ */
102
+ declare function useFlagValue<T extends boolean | string | number = boolean>(key: string, defaultValue?: T): T;
103
+ /**
104
+ * Get variant for A/B testing
105
+ * @example
106
+ * const variant = useVariant("checkout-experiment");
107
+ * if (variant === "control") return <OldCheckout />;
108
+ * if (variant === "treatment-a") return <NewCheckoutA />;
109
+ * return <NewCheckoutB />;
110
+ */
111
+ declare function useVariant(key: string): string | undefined;
72
112
 
73
- export { Databuddy, FlagState, FlagsConfig, FlagsProvider, useFlags };
113
+ export { Databuddy, FlagsProvider, useFeature, useFeatureOn, useFlag, useFlagValue, useFlags, useVariant };
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { i as isScriptInjected, c as createScript, B as BrowserFlagStorage, C as CoreFlagsManager, l as logger } from '../shared/@databuddy/sdk.ByNF_UxE.mjs';
3
+ import { i as isScriptInjected, c as createScript } from '../shared/@databuddy/sdk.F8Xt1uF7.mjs';
4
4
  import { d as detectClientId } from '../shared/@databuddy/sdk.BUsPV0LH.mjs';
5
- import { createStore, atom, Provider, useAtom } from 'jotai';
6
- import { useRef, useEffect, createElement } from 'react';
5
+ import React, { useRef, useMemo, useEffect, useSyncExternalStore, createContext, useContext } from 'react';
6
+ import { B as BrowserFlagStorage, C as CoreFlagsManager } from '../shared/@databuddy/sdk.Du7SE7-M.mjs';
7
+ import { l as logger } from '../shared/@databuddy/sdk.3kaCzyfu.mjs';
7
8
 
8
9
  function Databuddy(props) {
9
10
  const clientId = detectClientId(props.clientId);
@@ -22,101 +23,192 @@ function Databuddy(props) {
22
23
  return null;
23
24
  }
24
25
 
25
- const flagsStore = createStore();
26
- const managerAtom = atom(null);
27
- const memoryFlagsAtom = atom({});
26
+ const FlagsReactContext = createContext(null);
27
+ function createFlagState(result, isLoading, isPending) {
28
+ if (isPending) {
29
+ return {
30
+ on: false,
31
+ enabled: false,
32
+ status: "pending",
33
+ loading: true,
34
+ isLoading: true,
35
+ isReady: false
36
+ };
37
+ }
38
+ if (isLoading || !result) {
39
+ return {
40
+ on: false,
41
+ enabled: false,
42
+ status: "loading",
43
+ loading: true,
44
+ isLoading: true,
45
+ isReady: false
46
+ };
47
+ }
48
+ const status = result.reason === "ERROR" ? "error" : "ready";
49
+ return {
50
+ on: result.enabled,
51
+ enabled: result.enabled,
52
+ status,
53
+ loading: false,
54
+ isLoading: false,
55
+ isReady: true,
56
+ value: result.value,
57
+ variant: result.variant
58
+ };
59
+ }
28
60
  function FlagsProvider({ children, ...config }) {
29
- const managerRef = useRef(null);
30
- useEffect(() => {
61
+ const storeRef = useRef({ flags: {}, isReady: false });
62
+ const listenersRef = useRef(/* @__PURE__ */ new Set());
63
+ const manager = useMemo(() => {
31
64
  const storage = config.skipStorage ? void 0 : new BrowserFlagStorage();
32
- const manager = new CoreFlagsManager({
65
+ return new CoreFlagsManager({
33
66
  config,
34
67
  storage,
35
68
  onFlagsUpdate: (flags) => {
36
- flagsStore.set(memoryFlagsAtom, flags);
69
+ storeRef.current = { ...storeRef.current, flags };
70
+ for (const listener of listenersRef.current) {
71
+ listener();
72
+ }
73
+ },
74
+ onReady: () => {
75
+ storeRef.current = { ...storeRef.current, isReady: true };
76
+ for (const listener of listenersRef.current) {
77
+ listener();
78
+ }
37
79
  }
38
80
  });
39
- managerRef.current = manager;
40
- flagsStore.set(managerAtom, manager);
41
- return () => {
42
- managerRef.current = null;
43
- flagsStore.set(managerAtom, null);
44
- };
45
- }, [
46
- config.clientId,
47
- config.apiUrl,
48
- config.user?.userId,
49
- config.user?.email,
50
- config.disabled,
51
- config.debug,
52
- config.skipStorage,
53
- config.isPending,
54
- config.autoFetch
55
- ]);
81
+ }, [config.clientId]);
82
+ const prevConfigRef = useRef(config);
56
83
  useEffect(() => {
57
- if (managerRef.current) {
58
- managerRef.current.updateConfig(config);
84
+ const prevConfig = prevConfigRef.current;
85
+ const configChanged = prevConfig.apiUrl !== config.apiUrl || prevConfig.isPending !== config.isPending || prevConfig.user?.userId !== config.user?.userId || prevConfig.user?.email !== config.user?.email || prevConfig.environment !== config.environment || prevConfig.disabled !== config.disabled || prevConfig.autoFetch !== config.autoFetch || prevConfig.cacheTtl !== config.cacheTtl || prevConfig.staleTime !== config.staleTime;
86
+ if (configChanged) {
87
+ prevConfigRef.current = config;
88
+ manager.updateConfig(config);
59
89
  }
60
- }, [
61
- config.clientId,
62
- config.apiUrl,
63
- config.user?.userId,
64
- config.user?.email,
65
- config.disabled,
66
- config.debug,
67
- config.skipStorage,
68
- config.isPending,
69
- config.autoFetch
70
- ]);
71
- return createElement(Provider, { store: flagsStore }, children);
90
+ }, [manager, config]);
91
+ useEffect(() => {
92
+ return () => {
93
+ manager.destroy();
94
+ };
95
+ }, [manager]);
96
+ const subscribe = useMemo(
97
+ () => (callback) => {
98
+ listenersRef.current.add(callback);
99
+ return () => {
100
+ listenersRef.current.delete(callback);
101
+ };
102
+ },
103
+ []
104
+ );
105
+ const getSnapshot = useMemo(() => () => storeRef.current, []);
106
+ const store = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
107
+ const contextValue = useMemo(
108
+ () => ({
109
+ // Cleaner API: getFlag returns FlagState
110
+ getFlag: (key) => {
111
+ const result = store.flags[key];
112
+ const managerState = manager.isEnabled(key);
113
+ return createFlagState(
114
+ result,
115
+ managerState.isLoading,
116
+ config.isPending ?? false
117
+ );
118
+ },
119
+ // Simple boolean check
120
+ isOn: (key) => {
121
+ const result = store.flags[key];
122
+ if (result) {
123
+ return result.enabled;
124
+ }
125
+ const state = manager.isEnabled(key);
126
+ return state.enabled;
127
+ },
128
+ // Get typed value
129
+ getValue: (key, defaultValue) => {
130
+ const result = store.flags[key];
131
+ if (result) {
132
+ return result.value;
133
+ }
134
+ return manager.getValue(key, defaultValue);
135
+ },
136
+ // Async fetch
137
+ fetchFlag: (key) => manager.getFlag(key),
138
+ fetchAllFlags: () => manager.fetchAllFlags(),
139
+ updateUser: (user) => manager.updateUser(user),
140
+ refresh: (forceClear = false) => manager.refresh(forceClear),
141
+ isReady: store.isReady,
142
+ // Deprecated: kept for backwards compatibility
143
+ isEnabled: (key) => {
144
+ const result = store.flags[key];
145
+ const managerState = manager.isEnabled(key);
146
+ return createFlagState(
147
+ result,
148
+ managerState.isLoading,
149
+ config.isPending ?? false
150
+ );
151
+ }
152
+ }),
153
+ [manager, store, config.isPending]
154
+ );
155
+ return /* @__PURE__ */ React.createElement(FlagsReactContext.Provider, { value: contextValue }, children);
72
156
  }
73
157
  function useFlags() {
74
- const [manager] = useAtom(managerAtom, { store: flagsStore });
75
- const [memoryFlags] = useAtom(memoryFlagsAtom, {
76
- store: flagsStore
77
- });
78
- logger.debug("useFlags called with manager:", {
79
- hasManager: !!manager,
80
- memoryFlagsCount: Object.keys(memoryFlags).length,
81
- memoryFlags: Object.keys(memoryFlags)
82
- });
83
- const isEnabled = (key) => {
84
- if (!manager) {
85
- return {
158
+ const context = useContext(FlagsReactContext);
159
+ if (!context) {
160
+ logger.warn("useFlags called outside FlagsProvider");
161
+ return {
162
+ isEnabled: () => createFlagState(void 0, false, false),
163
+ getFlag: () => createFlagState(void 0, false, false),
164
+ isOn: () => false,
165
+ getValue: (_key, defaultValue) => defaultValue ?? false,
166
+ fetchFlag: async () => ({
86
167
  enabled: false,
87
- isLoading: false,
88
- isReady: false
89
- };
90
- }
91
- return manager.isEnabled(key);
92
- };
93
- const fetchAllFlags = () => {
94
- if (!manager) {
95
- logger.warn("No manager for bulk fetch");
96
- return;
97
- }
98
- return manager.fetchAllFlags();
99
- };
100
- const updateUser = (user) => {
101
- if (!manager) {
102
- logger.warn("No manager for user update");
103
- return;
104
- }
105
- manager.updateUser(user);
106
- };
107
- const refresh = (forceClear = false) => {
108
- if (!manager) {
109
- logger.warn("No manager for refresh");
110
- return;
111
- }
112
- manager.refresh(forceClear);
113
- };
168
+ value: false,
169
+ payload: null,
170
+ reason: "NO_PROVIDER"
171
+ }),
172
+ fetchAllFlags: async () => {
173
+ },
174
+ updateUser: () => {
175
+ },
176
+ refresh: async () => {
177
+ },
178
+ isReady: false
179
+ };
180
+ }
181
+ return context;
182
+ }
183
+ function useFlag(key) {
184
+ const { getFlag } = useFlags();
185
+ return getFlag(key);
186
+ }
187
+ function useFeature(key) {
188
+ const flag = useFlag(key);
114
189
  return {
115
- isEnabled,
116
- fetchAllFlags,
117
- updateUser,
118
- refresh
190
+ on: flag.on,
191
+ loading: flag.loading,
192
+ status: flag.status,
193
+ value: flag.value,
194
+ variant: flag.variant
119
195
  };
120
196
  }
197
+ function useFeatureOn(key, defaultValue = false) {
198
+ const { isOn, isReady } = useFlags();
199
+ const flag = useFlag(key);
200
+ if (flag.loading || !isReady) {
201
+ return defaultValue;
202
+ }
203
+ return isOn(key);
204
+ }
205
+ function useFlagValue(key, defaultValue) {
206
+ const { getValue } = useFlags();
207
+ return getValue(key, defaultValue);
208
+ }
209
+ function useVariant(key) {
210
+ const flag = useFlag(key);
211
+ return flag.variant;
212
+ }
121
213
 
122
- export { Databuddy, FlagsProvider, useFlags };
214
+ export { Databuddy, FlagsProvider, useFeature, useFeatureOn, useFlag, useFlagValue, useFlags, useVariant };
@@ -0,0 +1,199 @@
1
+ class Logger {
2
+ debugEnabled = false;
3
+ /**
4
+ * Enable or disable debug logging
5
+ */
6
+ setDebug(enabled) {
7
+ this.debugEnabled = enabled;
8
+ }
9
+ /**
10
+ * Log debug messages (only when debug is enabled)
11
+ */
12
+ debug(...args) {
13
+ if (this.debugEnabled) {
14
+ console.log("[Databuddy]", ...args);
15
+ }
16
+ }
17
+ /**
18
+ * Log info messages (always enabled)
19
+ */
20
+ info(...args) {
21
+ console.info("[Databuddy]", ...args);
22
+ }
23
+ /**
24
+ * Log warning messages (always enabled)
25
+ */
26
+ warn(...args) {
27
+ console.warn("[Databuddy]", ...args);
28
+ }
29
+ /**
30
+ * Log error messages (always enabled)
31
+ */
32
+ error(...args) {
33
+ console.error("[Databuddy]", ...args);
34
+ }
35
+ /**
36
+ * Log with table format (only when debug is enabled)
37
+ */
38
+ table(data) {
39
+ if (this.debugEnabled) {
40
+ console.table(data);
41
+ }
42
+ }
43
+ /**
44
+ * Time a function execution (only when debug is enabled)
45
+ */
46
+ time(label) {
47
+ if (this.debugEnabled) {
48
+ console.time(`[Databuddy] ${label}`);
49
+ }
50
+ }
51
+ /**
52
+ * End timing a function execution (only when debug is enabled)
53
+ */
54
+ timeEnd(label) {
55
+ if (this.debugEnabled) {
56
+ console.timeEnd(`[Databuddy] ${label}`);
57
+ }
58
+ }
59
+ /**
60
+ * Log JSON data (only when debug is enabled)
61
+ */
62
+ json(data) {
63
+ if (this.debugEnabled) {
64
+ console.log("[Databuddy]", JSON.stringify(data, null, 2));
65
+ }
66
+ }
67
+ }
68
+ const logger = new Logger();
69
+
70
+ const DEFAULT_RESULT = {
71
+ enabled: false,
72
+ value: false,
73
+ payload: null,
74
+ reason: "DEFAULT"
75
+ };
76
+ function getCacheKey(key, user) {
77
+ if (!(user?.userId || user?.email)) {
78
+ return key;
79
+ }
80
+ return `${key}:${user.userId ?? ""}:${user.email ?? ""}`;
81
+ }
82
+ function buildQueryParams(config, user) {
83
+ const params = new URLSearchParams();
84
+ params.set("clientId", config.clientId);
85
+ const u = user ?? config.user;
86
+ if (u?.userId) {
87
+ params.set("userId", u.userId);
88
+ }
89
+ if (u?.email) {
90
+ params.set("email", u.email);
91
+ }
92
+ if (u?.properties) {
93
+ params.set("properties", JSON.stringify(u.properties));
94
+ }
95
+ if (config.environment) {
96
+ params.set("environment", config.environment);
97
+ }
98
+ return params;
99
+ }
100
+ async function fetchFlags(apiUrl, keys, params) {
101
+ const batchParams = new URLSearchParams(params);
102
+ batchParams.set("keys", keys.join(","));
103
+ const url = `${apiUrl}/public/v1/flags/bulk?${batchParams}`;
104
+ const response = await fetch(url);
105
+ if (!response.ok) {
106
+ const result = {};
107
+ for (const key of keys) {
108
+ result[key] = { ...DEFAULT_RESULT, reason: "ERROR" };
109
+ }
110
+ return result;
111
+ }
112
+ const data = await response.json();
113
+ return data.flags ?? {};
114
+ }
115
+ async function fetchAllFlags(apiUrl, params) {
116
+ const url = `${apiUrl}/public/v1/flags/bulk?${params}`;
117
+ const response = await fetch(url);
118
+ if (!response.ok) {
119
+ return {};
120
+ }
121
+ const data = await response.json();
122
+ return data.flags ?? {};
123
+ }
124
+ function isCacheValid(entry) {
125
+ if (!entry) {
126
+ return false;
127
+ }
128
+ return Date.now() <= entry.expiresAt;
129
+ }
130
+ function isCacheStale(entry) {
131
+ return Date.now() > entry.staleAt;
132
+ }
133
+ function createCacheEntry(result, ttl, staleTime) {
134
+ const now = Date.now();
135
+ return {
136
+ result,
137
+ staleAt: now + (staleTime ?? ttl / 2),
138
+ expiresAt: now + ttl
139
+ };
140
+ }
141
+ class RequestBatcher {
142
+ pending = /* @__PURE__ */ new Map();
143
+ timer = null;
144
+ batchDelayMs;
145
+ apiUrl;
146
+ params;
147
+ constructor(apiUrl, params, batchDelayMs = 10) {
148
+ this.apiUrl = apiUrl;
149
+ this.params = params;
150
+ this.batchDelayMs = batchDelayMs;
151
+ }
152
+ request(key) {
153
+ return new Promise((resolve, reject) => {
154
+ const existing = this.pending.get(key);
155
+ if (existing) {
156
+ existing.push({ resolve, reject });
157
+ } else {
158
+ this.pending.set(key, [{ resolve, reject }]);
159
+ }
160
+ if (!this.timer) {
161
+ this.timer = setTimeout(() => this.flush(), this.batchDelayMs);
162
+ }
163
+ });
164
+ }
165
+ async flush() {
166
+ this.timer = null;
167
+ const keys = [...this.pending.keys()];
168
+ const callbacks = new Map(this.pending);
169
+ this.pending.clear();
170
+ if (keys.length === 0) {
171
+ return;
172
+ }
173
+ try {
174
+ const results = await fetchFlags(this.apiUrl, keys, this.params);
175
+ for (const [key, cbs] of callbacks) {
176
+ const result = results[key] ?? { ...DEFAULT_RESULT, reason: "NOT_FOUND" };
177
+ for (const cb of cbs) {
178
+ cb.resolve(result);
179
+ }
180
+ }
181
+ } catch (err) {
182
+ const error = err instanceof Error ? err : new Error("Fetch failed");
183
+ for (const cbs of callbacks.values()) {
184
+ for (const cb of cbs) {
185
+ cb.reject(error);
186
+ }
187
+ }
188
+ }
189
+ }
190
+ destroy() {
191
+ if (this.timer) {
192
+ clearTimeout(this.timer);
193
+ this.timer = null;
194
+ }
195
+ this.pending.clear();
196
+ }
197
+ }
198
+
199
+ export { DEFAULT_RESULT as D, RequestBatcher as R, isCacheStale as a, buildQueryParams as b, createCacheEntry as c, fetchAllFlags as f, getCacheKey as g, isCacheValid as i, logger as l };