@savvagent/sdk 1.0.1 → 1.1.0

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.
package/README.md CHANGED
@@ -159,6 +159,9 @@ interface FlagClientConfig {
159
159
  /** Base URL for the Savvagent API (default: production) */
160
160
  baseUrl?: string;
161
161
 
162
+ /** Environment for flag evaluation (default: 'production') */
163
+ environment?: string;
164
+
162
165
  /** Enable real-time flag updates via SSE (default: true) */
163
166
  enableRealtime?: boolean;
164
167
 
@@ -182,6 +185,7 @@ interface FlagClientConfig {
182
185
  const client = new FlagClient({
183
186
  apiKey: 'sdk_dev_abc123',
184
187
  baseUrl: 'https://api.savvagent.com',
188
+ environment: 'staging', // Use staging environment flags
185
189
  enableRealtime: true,
186
190
  cacheTtl: 30000, // 30 seconds
187
191
  enableTelemetry: true,
@@ -196,6 +200,47 @@ const client = new FlagClient({
196
200
  });
197
201
  ```
198
202
 
203
+ ### Environment Configuration
204
+
205
+ The `environment` option controls which environment's flag values are used during evaluation. Common values include:
206
+
207
+ - `'production'` (default) - Production environment
208
+ - `'staging'` - Staging/pre-production environment
209
+ - `'development'` - Local development environment
210
+ - `'beta'` - Beta testing environment
211
+
212
+ ```typescript
213
+ // Production app
214
+ const client = new FlagClient({
215
+ apiKey: 'sdk_prod_...',
216
+ environment: 'production',
217
+ });
218
+
219
+ // Staging/QA app
220
+ const client = new FlagClient({
221
+ apiKey: 'sdk_staging_...',
222
+ environment: 'staging',
223
+ });
224
+
225
+ // Development
226
+ const client = new FlagClient({
227
+ apiKey: 'sdk_dev_...',
228
+ environment: 'development',
229
+ });
230
+ ```
231
+
232
+ You can also change the environment at runtime:
233
+
234
+ ```typescript
235
+ // Switch to beta environment
236
+ client.setEnvironment('beta');
237
+
238
+ // Get current environment
239
+ console.log(client.getEnvironment()); // 'beta'
240
+ ```
241
+
242
+ Note: Changing the environment clears the cache since flag values may differ between environments.
243
+
199
244
  ## Framework Integration
200
245
 
201
246
  ### React
@@ -408,6 +453,10 @@ const client = new FlagClient({
408
453
  - `withFlag(flagKey, callback, context?)`: Execute code conditionally
409
454
  - `trackError(flagKey, error, context?)`: Manually track an error
410
455
  - `subscribe(flagKey, callback)`: Subscribe to flag updates
456
+ - `setEnvironment(environment)`: Set the environment for flag evaluation
457
+ - `getEnvironment()`: Get the current environment
458
+ - `setUserId(userId)`: Set the user ID for targeted rollouts
459
+ - `getUserId()`: Get the current user ID
411
460
  - `getCachedFlags()`: Get all cached flag keys
412
461
  - `clearCache()`: Clear the flag cache
413
462
  - `isRealtimeConnected()`: Check real-time connection status
package/dist/index.d.mts CHANGED
@@ -11,6 +11,8 @@ interface FlagClientConfig {
11
11
  applicationId?: string;
12
12
  /** Base URL for the Savvagent API (default: production URL) */
13
13
  baseUrl?: string;
14
+ /** Environment for flag evaluation (e.g., "development", "staging", "production", "beta"). Default: "production" */
15
+ environment?: string;
14
16
  /** Enable real-time flag updates via SSE (default: true) */
15
17
  enableRealtime?: boolean;
16
18
  /** Cache TTL in milliseconds (default: 60000 = 1 minute) */
@@ -25,6 +27,12 @@ interface FlagClientConfig {
25
27
  defaultLanguage?: string;
26
28
  /** Disable automatic browser language detection (default: false) */
27
29
  disableLanguageDetection?: boolean;
30
+ /** Number of retry attempts for transient failures (default: 3) */
31
+ retryAttempts?: number;
32
+ /** Base delay between retries in milliseconds (default: 1000) */
33
+ retryDelay?: number;
34
+ /** Retry backoff strategy: 'linear' or 'exponential' (default: 'exponential') */
35
+ retryBackoff?: 'linear' | 'exponential';
28
36
  }
29
37
  /**
30
38
  * Context passed to flag evaluation
@@ -182,6 +190,16 @@ declare class FlagClient {
182
190
  * Get the current user ID
183
191
  */
184
192
  getUserId(): string | null;
193
+ /**
194
+ * Set the environment for flag evaluation
195
+ * Useful for dynamically switching environments (e.g., dev tools)
196
+ * @param environment - The environment name (e.g., "development", "staging", "production", "beta")
197
+ */
198
+ setEnvironment(environment: string): void;
199
+ /**
200
+ * Get the current environment
201
+ */
202
+ getEnvironment(): string;
185
203
  /**
186
204
  * Get the current anonymous ID
187
205
  */
@@ -191,6 +209,23 @@ declare class FlagClient {
191
209
  * @param overrides - Context overrides
192
210
  */
193
211
  private buildContext;
212
+ /**
213
+ * Check if an error is retryable (transient failure)
214
+ * @param error - The error to check
215
+ * @param status - HTTP status code (if available)
216
+ */
217
+ private isRetryableError;
218
+ /**
219
+ * Calculate delay for retry attempt
220
+ * @param attempt - Current attempt number (1-based)
221
+ */
222
+ private getRetryDelay;
223
+ /**
224
+ * Execute a fetch request with retry logic
225
+ * @param requestFn - Function that returns a fetch promise
226
+ * @param operationName - Name of the operation for logging
227
+ */
228
+ private fetchWithRetry;
194
229
  /**
195
230
  * Check if a feature flag is enabled
196
231
  * @param flagKey - The flag key to evaluate
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ interface FlagClientConfig {
11
11
  applicationId?: string;
12
12
  /** Base URL for the Savvagent API (default: production URL) */
13
13
  baseUrl?: string;
14
+ /** Environment for flag evaluation (e.g., "development", "staging", "production", "beta"). Default: "production" */
15
+ environment?: string;
14
16
  /** Enable real-time flag updates via SSE (default: true) */
15
17
  enableRealtime?: boolean;
16
18
  /** Cache TTL in milliseconds (default: 60000 = 1 minute) */
@@ -25,6 +27,12 @@ interface FlagClientConfig {
25
27
  defaultLanguage?: string;
26
28
  /** Disable automatic browser language detection (default: false) */
27
29
  disableLanguageDetection?: boolean;
30
+ /** Number of retry attempts for transient failures (default: 3) */
31
+ retryAttempts?: number;
32
+ /** Base delay between retries in milliseconds (default: 1000) */
33
+ retryDelay?: number;
34
+ /** Retry backoff strategy: 'linear' or 'exponential' (default: 'exponential') */
35
+ retryBackoff?: 'linear' | 'exponential';
28
36
  }
29
37
  /**
30
38
  * Context passed to flag evaluation
@@ -182,6 +190,16 @@ declare class FlagClient {
182
190
  * Get the current user ID
183
191
  */
184
192
  getUserId(): string | null;
193
+ /**
194
+ * Set the environment for flag evaluation
195
+ * Useful for dynamically switching environments (e.g., dev tools)
196
+ * @param environment - The environment name (e.g., "development", "staging", "production", "beta")
197
+ */
198
+ setEnvironment(environment: string): void;
199
+ /**
200
+ * Get the current environment
201
+ */
202
+ getEnvironment(): string;
185
203
  /**
186
204
  * Get the current anonymous ID
187
205
  */
@@ -191,6 +209,23 @@ declare class FlagClient {
191
209
  * @param overrides - Context overrides
192
210
  */
193
211
  private buildContext;
212
+ /**
213
+ * Check if an error is retryable (transient failure)
214
+ * @param error - The error to check
215
+ * @param status - HTTP status code (if available)
216
+ */
217
+ private isRetryableError;
218
+ /**
219
+ * Calculate delay for retry attempt
220
+ * @param attempt - Current attempt number (1-based)
221
+ */
222
+ private getRetryDelay;
223
+ /**
224
+ * Execute a fetch request with retry logic
225
+ * @param requestFn - Function that returns a fetch promise
226
+ * @param operationName - Name of the operation for logging
227
+ */
228
+ private fetchWithRetry;
194
229
  /**
195
230
  * Check if a feature flag is enabled
196
231
  * @param flagKey - The flag key to evaluate
package/dist/index.js CHANGED
@@ -422,13 +422,17 @@ var FlagClient = class {
422
422
  apiKey: config.apiKey,
423
423
  applicationId: config.applicationId || "",
424
424
  baseUrl: config.baseUrl || "http://localhost:8080",
425
+ environment: config.environment || "production",
425
426
  enableRealtime: config.enableRealtime ?? true,
426
427
  cacheTtl: config.cacheTtl || 6e4,
427
428
  enableTelemetry: config.enableTelemetry ?? true,
428
429
  defaults: config.defaults || {},
429
430
  onError: config.onError || ((error) => console.error("[Savvagent]", error)),
430
431
  defaultLanguage: config.defaultLanguage || "",
431
- disableLanguageDetection: config.disableLanguageDetection ?? false
432
+ disableLanguageDetection: config.disableLanguageDetection ?? false,
433
+ retryAttempts: config.retryAttempts ?? 3,
434
+ retryDelay: config.retryDelay ?? 1e3,
435
+ retryBackoff: config.retryBackoff ?? "exponential"
432
436
  };
433
437
  if (!this.config.disableLanguageDetection && typeof navigator !== "undefined") {
434
438
  this.detectedLanguage = this.config.defaultLanguage || navigator.language || navigator.userLanguage || null;
@@ -507,6 +511,21 @@ var FlagClient = class {
507
511
  getUserId() {
508
512
  return this.userId;
509
513
  }
514
+ /**
515
+ * Set the environment for flag evaluation
516
+ * Useful for dynamically switching environments (e.g., dev tools)
517
+ * @param environment - The environment name (e.g., "development", "staging", "production", "beta")
518
+ */
519
+ setEnvironment(environment) {
520
+ this.config.environment = environment;
521
+ this.cache.clear();
522
+ }
523
+ /**
524
+ * Get the current environment
525
+ */
526
+ getEnvironment() {
527
+ return this.config.environment;
528
+ }
510
529
  /**
511
530
  * Get the current anonymous ID
512
531
  */
@@ -521,8 +540,7 @@ var FlagClient = class {
521
540
  const context = {
522
541
  user_id: this.userId || void 0,
523
542
  anonymous_id: this.anonymousId || void 0,
524
- environment: "production",
525
- // TODO: Make configurable
543
+ environment: this.config.environment,
526
544
  ...overrides
527
545
  };
528
546
  if (!context.application_id && this.config.applicationId) {
@@ -533,6 +551,86 @@ var FlagClient = class {
533
551
  }
534
552
  return context;
535
553
  }
554
+ /**
555
+ * Check if an error is retryable (transient failure)
556
+ * @param error - The error to check
557
+ * @param status - HTTP status code (if available)
558
+ */
559
+ isRetryableError(error, status) {
560
+ if (status === 401 || status === 403) {
561
+ return false;
562
+ }
563
+ if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) {
564
+ return false;
565
+ }
566
+ if (status && status >= 500) {
567
+ return true;
568
+ }
569
+ if (error.name === "AbortError" || error.name === "TypeError" || error.message.includes("network")) {
570
+ return true;
571
+ }
572
+ return false;
573
+ }
574
+ /**
575
+ * Calculate delay for retry attempt
576
+ * @param attempt - Current attempt number (1-based)
577
+ */
578
+ getRetryDelay(attempt) {
579
+ const baseDelay = this.config.retryDelay;
580
+ if (this.config.retryBackoff === "linear") {
581
+ return baseDelay * attempt;
582
+ }
583
+ const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
584
+ const jitter = Math.random() * 0.3 * exponentialDelay;
585
+ return exponentialDelay + jitter;
586
+ }
587
+ /**
588
+ * Execute a fetch request with retry logic
589
+ * @param requestFn - Function that returns a fetch promise
590
+ * @param operationName - Name of the operation for logging
591
+ */
592
+ async fetchWithRetry(requestFn, operationName) {
593
+ let lastError = null;
594
+ let lastStatus;
595
+ for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
596
+ try {
597
+ const response = await requestFn();
598
+ if (response.status === 401 || response.status === 403) {
599
+ this.authFailed = true;
600
+ this.realtime?.disconnect();
601
+ console.error(`[Savvagent] Authentication failed (${response.status}). Check your API key. Further requests disabled.`);
602
+ throw new Error(`Authentication failed: ${response.status}`);
603
+ }
604
+ if (response.ok) {
605
+ return response;
606
+ }
607
+ lastStatus = response.status;
608
+ lastError = new Error(`${operationName} failed: ${response.status}`);
609
+ if (!this.isRetryableError(lastError, response.status)) {
610
+ throw lastError;
611
+ }
612
+ if (attempt < this.config.retryAttempts) {
613
+ const delay = this.getRetryDelay(attempt);
614
+ console.warn(`[Savvagent] ${operationName} failed (${response.status}), retrying in ${Math.round(delay)}ms (attempt ${attempt}/${this.config.retryAttempts})`);
615
+ await new Promise((resolve) => setTimeout(resolve, delay));
616
+ }
617
+ } catch (error) {
618
+ lastError = error;
619
+ if (lastError.message.includes("Authentication failed")) {
620
+ throw lastError;
621
+ }
622
+ if (!this.isRetryableError(lastError, lastStatus)) {
623
+ throw lastError;
624
+ }
625
+ if (attempt < this.config.retryAttempts) {
626
+ const delay = this.getRetryDelay(attempt);
627
+ console.warn(`[Savvagent] ${operationName} error: ${lastError.message}, retrying in ${Math.round(delay)}ms (attempt ${attempt}/${this.config.retryAttempts})`);
628
+ await new Promise((resolve) => setTimeout(resolve, delay));
629
+ }
630
+ }
631
+ }
632
+ throw lastError || new Error(`${operationName} failed after ${this.config.retryAttempts} attempts`);
633
+ }
536
634
  /**
537
635
  * Check if a feature flag is enabled
538
636
  * @param flagKey - The flag key to evaluate
@@ -588,29 +686,26 @@ var FlagClient = class {
588
686
  const requestBody = {
589
687
  context: evaluationContext
590
688
  };
591
- const controller = new AbortController();
592
- const timeoutId = setTimeout(() => {
593
- controller.abort();
594
- }, 1e4);
595
- const response = await fetch(`${this.config.baseUrl}/api/flags/${flagKey}/evaluate`, {
596
- method: "POST",
597
- headers: {
598
- "Content-Type": "application/json",
599
- Authorization: `Bearer ${this.config.apiKey}`
689
+ const response = await this.fetchWithRetry(
690
+ () => {
691
+ const controller = new AbortController();
692
+ const timeoutId = setTimeout(() => {
693
+ controller.abort();
694
+ }, 1e4);
695
+ return fetch(`${this.config.baseUrl}/api/flags/${flagKey}/evaluate`, {
696
+ method: "POST",
697
+ headers: {
698
+ "Content-Type": "application/json",
699
+ Authorization: `Bearer ${this.config.apiKey}`
700
+ },
701
+ body: JSON.stringify(requestBody),
702
+ signal: controller.signal
703
+ }).finally(() => {
704
+ clearTimeout(timeoutId);
705
+ });
600
706
  },
601
- body: JSON.stringify(requestBody),
602
- signal: controller.signal
603
- });
604
- clearTimeout(timeoutId);
605
- if (!response.ok) {
606
- if (response.status === 401 || response.status === 403) {
607
- this.authFailed = true;
608
- this.realtime?.disconnect();
609
- console.error(`[Savvagent] Authentication failed (${response.status}). Check your API key. Further requests disabled.`);
610
- throw new Error(`Authentication failed: ${response.status}`);
611
- }
612
- throw new Error(`Flag evaluation failed: ${response.status}`);
613
- }
707
+ `Flag evaluation (${flagKey})`
708
+ );
614
709
  const data = await response.json();
615
710
  const value = data.enabled || false;
616
711
  this.cache.set(flagKey, value, data.key);
@@ -867,30 +962,27 @@ var FlagClient = class {
867
962
  return [];
868
963
  }
869
964
  try {
870
- const controller = new AbortController();
871
- const timeoutId = setTimeout(() => {
872
- controller.abort();
873
- }, 1e4);
874
- const response = await fetch(
875
- `${this.config.baseUrl}/api/sdk/flags?environment=${encodeURIComponent(environment)}`,
876
- {
877
- method: "GET",
878
- headers: {
879
- Authorization: `Bearer ${this.config.apiKey}`
880
- },
881
- signal: controller.signal
882
- }
965
+ const response = await this.fetchWithRetry(
966
+ () => {
967
+ const controller = new AbortController();
968
+ const timeoutId = setTimeout(() => {
969
+ controller.abort();
970
+ }, 1e4);
971
+ return fetch(
972
+ `${this.config.baseUrl}/api/sdk/flags?environment=${encodeURIComponent(environment)}`,
973
+ {
974
+ method: "GET",
975
+ headers: {
976
+ Authorization: `Bearer ${this.config.apiKey}`
977
+ },
978
+ signal: controller.signal
979
+ }
980
+ ).finally(() => {
981
+ clearTimeout(timeoutId);
982
+ });
983
+ },
984
+ "Get all flags"
883
985
  );
884
- clearTimeout(timeoutId);
885
- if (!response.ok) {
886
- if (response.status === 401 || response.status === 403) {
887
- this.authFailed = true;
888
- this.realtime?.disconnect();
889
- console.error(`[Savvagent] Authentication failed (${response.status}). Check your API key. Further requests disabled.`);
890
- throw new Error(`Authentication failed: ${response.status}`);
891
- }
892
- throw new Error(`Failed to fetch flags: ${response.status}`);
893
- }
894
986
  const data = await response.json();
895
987
  data.flags.forEach((flag) => {
896
988
  this.cache.set(flag.key, flag.enabled, flag.key);
@@ -921,30 +1013,27 @@ var FlagClient = class {
921
1013
  return [];
922
1014
  }
923
1015
  try {
924
- const controller = new AbortController();
925
- const timeoutId = setTimeout(() => {
926
- controller.abort();
927
- }, 1e4);
928
- const response = await fetch(
929
- `${this.config.baseUrl}/api/sdk/enterprise-flags?environment=${encodeURIComponent(environment)}`,
930
- {
931
- method: "GET",
932
- headers: {
933
- Authorization: `Bearer ${this.config.apiKey}`
934
- },
935
- signal: controller.signal
936
- }
1016
+ const response = await this.fetchWithRetry(
1017
+ () => {
1018
+ const controller = new AbortController();
1019
+ const timeoutId = setTimeout(() => {
1020
+ controller.abort();
1021
+ }, 1e4);
1022
+ return fetch(
1023
+ `${this.config.baseUrl}/api/sdk/enterprise-flags?environment=${encodeURIComponent(environment)}`,
1024
+ {
1025
+ method: "GET",
1026
+ headers: {
1027
+ Authorization: `Bearer ${this.config.apiKey}`
1028
+ },
1029
+ signal: controller.signal
1030
+ }
1031
+ ).finally(() => {
1032
+ clearTimeout(timeoutId);
1033
+ });
1034
+ },
1035
+ "Get enterprise flags"
937
1036
  );
938
- clearTimeout(timeoutId);
939
- if (!response.ok) {
940
- if (response.status === 401 || response.status === 403) {
941
- this.authFailed = true;
942
- this.realtime?.disconnect();
943
- console.error(`[Savvagent] Authentication failed (${response.status}). Check your API key. Further requests disabled.`);
944
- throw new Error(`Authentication failed: ${response.status}`);
945
- }
946
- throw new Error(`Failed to fetch enterprise flags: ${response.status}`);
947
- }
948
1037
  const data = await response.json();
949
1038
  return data.flags;
950
1039
  } catch (error) {
package/dist/index.mjs CHANGED
@@ -393,13 +393,17 @@ var FlagClient = class {
393
393
  apiKey: config.apiKey,
394
394
  applicationId: config.applicationId || "",
395
395
  baseUrl: config.baseUrl || "http://localhost:8080",
396
+ environment: config.environment || "production",
396
397
  enableRealtime: config.enableRealtime ?? true,
397
398
  cacheTtl: config.cacheTtl || 6e4,
398
399
  enableTelemetry: config.enableTelemetry ?? true,
399
400
  defaults: config.defaults || {},
400
401
  onError: config.onError || ((error) => console.error("[Savvagent]", error)),
401
402
  defaultLanguage: config.defaultLanguage || "",
402
- disableLanguageDetection: config.disableLanguageDetection ?? false
403
+ disableLanguageDetection: config.disableLanguageDetection ?? false,
404
+ retryAttempts: config.retryAttempts ?? 3,
405
+ retryDelay: config.retryDelay ?? 1e3,
406
+ retryBackoff: config.retryBackoff ?? "exponential"
403
407
  };
404
408
  if (!this.config.disableLanguageDetection && typeof navigator !== "undefined") {
405
409
  this.detectedLanguage = this.config.defaultLanguage || navigator.language || navigator.userLanguage || null;
@@ -478,6 +482,21 @@ var FlagClient = class {
478
482
  getUserId() {
479
483
  return this.userId;
480
484
  }
485
+ /**
486
+ * Set the environment for flag evaluation
487
+ * Useful for dynamically switching environments (e.g., dev tools)
488
+ * @param environment - The environment name (e.g., "development", "staging", "production", "beta")
489
+ */
490
+ setEnvironment(environment) {
491
+ this.config.environment = environment;
492
+ this.cache.clear();
493
+ }
494
+ /**
495
+ * Get the current environment
496
+ */
497
+ getEnvironment() {
498
+ return this.config.environment;
499
+ }
481
500
  /**
482
501
  * Get the current anonymous ID
483
502
  */
@@ -492,8 +511,7 @@ var FlagClient = class {
492
511
  const context = {
493
512
  user_id: this.userId || void 0,
494
513
  anonymous_id: this.anonymousId || void 0,
495
- environment: "production",
496
- // TODO: Make configurable
514
+ environment: this.config.environment,
497
515
  ...overrides
498
516
  };
499
517
  if (!context.application_id && this.config.applicationId) {
@@ -504,6 +522,86 @@ var FlagClient = class {
504
522
  }
505
523
  return context;
506
524
  }
525
+ /**
526
+ * Check if an error is retryable (transient failure)
527
+ * @param error - The error to check
528
+ * @param status - HTTP status code (if available)
529
+ */
530
+ isRetryableError(error, status) {
531
+ if (status === 401 || status === 403) {
532
+ return false;
533
+ }
534
+ if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) {
535
+ return false;
536
+ }
537
+ if (status && status >= 500) {
538
+ return true;
539
+ }
540
+ if (error.name === "AbortError" || error.name === "TypeError" || error.message.includes("network")) {
541
+ return true;
542
+ }
543
+ return false;
544
+ }
545
+ /**
546
+ * Calculate delay for retry attempt
547
+ * @param attempt - Current attempt number (1-based)
548
+ */
549
+ getRetryDelay(attempt) {
550
+ const baseDelay = this.config.retryDelay;
551
+ if (this.config.retryBackoff === "linear") {
552
+ return baseDelay * attempt;
553
+ }
554
+ const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
555
+ const jitter = Math.random() * 0.3 * exponentialDelay;
556
+ return exponentialDelay + jitter;
557
+ }
558
+ /**
559
+ * Execute a fetch request with retry logic
560
+ * @param requestFn - Function that returns a fetch promise
561
+ * @param operationName - Name of the operation for logging
562
+ */
563
+ async fetchWithRetry(requestFn, operationName) {
564
+ let lastError = null;
565
+ let lastStatus;
566
+ for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
567
+ try {
568
+ const response = await requestFn();
569
+ if (response.status === 401 || response.status === 403) {
570
+ this.authFailed = true;
571
+ this.realtime?.disconnect();
572
+ console.error(`[Savvagent] Authentication failed (${response.status}). Check your API key. Further requests disabled.`);
573
+ throw new Error(`Authentication failed: ${response.status}`);
574
+ }
575
+ if (response.ok) {
576
+ return response;
577
+ }
578
+ lastStatus = response.status;
579
+ lastError = new Error(`${operationName} failed: ${response.status}`);
580
+ if (!this.isRetryableError(lastError, response.status)) {
581
+ throw lastError;
582
+ }
583
+ if (attempt < this.config.retryAttempts) {
584
+ const delay = this.getRetryDelay(attempt);
585
+ console.warn(`[Savvagent] ${operationName} failed (${response.status}), retrying in ${Math.round(delay)}ms (attempt ${attempt}/${this.config.retryAttempts})`);
586
+ await new Promise((resolve) => setTimeout(resolve, delay));
587
+ }
588
+ } catch (error) {
589
+ lastError = error;
590
+ if (lastError.message.includes("Authentication failed")) {
591
+ throw lastError;
592
+ }
593
+ if (!this.isRetryableError(lastError, lastStatus)) {
594
+ throw lastError;
595
+ }
596
+ if (attempt < this.config.retryAttempts) {
597
+ const delay = this.getRetryDelay(attempt);
598
+ console.warn(`[Savvagent] ${operationName} error: ${lastError.message}, retrying in ${Math.round(delay)}ms (attempt ${attempt}/${this.config.retryAttempts})`);
599
+ await new Promise((resolve) => setTimeout(resolve, delay));
600
+ }
601
+ }
602
+ }
603
+ throw lastError || new Error(`${operationName} failed after ${this.config.retryAttempts} attempts`);
604
+ }
507
605
  /**
508
606
  * Check if a feature flag is enabled
509
607
  * @param flagKey - The flag key to evaluate
@@ -559,29 +657,26 @@ var FlagClient = class {
559
657
  const requestBody = {
560
658
  context: evaluationContext
561
659
  };
562
- const controller = new AbortController();
563
- const timeoutId = setTimeout(() => {
564
- controller.abort();
565
- }, 1e4);
566
- const response = await fetch(`${this.config.baseUrl}/api/flags/${flagKey}/evaluate`, {
567
- method: "POST",
568
- headers: {
569
- "Content-Type": "application/json",
570
- Authorization: `Bearer ${this.config.apiKey}`
660
+ const response = await this.fetchWithRetry(
661
+ () => {
662
+ const controller = new AbortController();
663
+ const timeoutId = setTimeout(() => {
664
+ controller.abort();
665
+ }, 1e4);
666
+ return fetch(`${this.config.baseUrl}/api/flags/${flagKey}/evaluate`, {
667
+ method: "POST",
668
+ headers: {
669
+ "Content-Type": "application/json",
670
+ Authorization: `Bearer ${this.config.apiKey}`
671
+ },
672
+ body: JSON.stringify(requestBody),
673
+ signal: controller.signal
674
+ }).finally(() => {
675
+ clearTimeout(timeoutId);
676
+ });
571
677
  },
572
- body: JSON.stringify(requestBody),
573
- signal: controller.signal
574
- });
575
- clearTimeout(timeoutId);
576
- if (!response.ok) {
577
- if (response.status === 401 || response.status === 403) {
578
- this.authFailed = true;
579
- this.realtime?.disconnect();
580
- console.error(`[Savvagent] Authentication failed (${response.status}). Check your API key. Further requests disabled.`);
581
- throw new Error(`Authentication failed: ${response.status}`);
582
- }
583
- throw new Error(`Flag evaluation failed: ${response.status}`);
584
- }
678
+ `Flag evaluation (${flagKey})`
679
+ );
585
680
  const data = await response.json();
586
681
  const value = data.enabled || false;
587
682
  this.cache.set(flagKey, value, data.key);
@@ -838,30 +933,27 @@ var FlagClient = class {
838
933
  return [];
839
934
  }
840
935
  try {
841
- const controller = new AbortController();
842
- const timeoutId = setTimeout(() => {
843
- controller.abort();
844
- }, 1e4);
845
- const response = await fetch(
846
- `${this.config.baseUrl}/api/sdk/flags?environment=${encodeURIComponent(environment)}`,
847
- {
848
- method: "GET",
849
- headers: {
850
- Authorization: `Bearer ${this.config.apiKey}`
851
- },
852
- signal: controller.signal
853
- }
936
+ const response = await this.fetchWithRetry(
937
+ () => {
938
+ const controller = new AbortController();
939
+ const timeoutId = setTimeout(() => {
940
+ controller.abort();
941
+ }, 1e4);
942
+ return fetch(
943
+ `${this.config.baseUrl}/api/sdk/flags?environment=${encodeURIComponent(environment)}`,
944
+ {
945
+ method: "GET",
946
+ headers: {
947
+ Authorization: `Bearer ${this.config.apiKey}`
948
+ },
949
+ signal: controller.signal
950
+ }
951
+ ).finally(() => {
952
+ clearTimeout(timeoutId);
953
+ });
954
+ },
955
+ "Get all flags"
854
956
  );
855
- clearTimeout(timeoutId);
856
- if (!response.ok) {
857
- if (response.status === 401 || response.status === 403) {
858
- this.authFailed = true;
859
- this.realtime?.disconnect();
860
- console.error(`[Savvagent] Authentication failed (${response.status}). Check your API key. Further requests disabled.`);
861
- throw new Error(`Authentication failed: ${response.status}`);
862
- }
863
- throw new Error(`Failed to fetch flags: ${response.status}`);
864
- }
865
957
  const data = await response.json();
866
958
  data.flags.forEach((flag) => {
867
959
  this.cache.set(flag.key, flag.enabled, flag.key);
@@ -892,30 +984,27 @@ var FlagClient = class {
892
984
  return [];
893
985
  }
894
986
  try {
895
- const controller = new AbortController();
896
- const timeoutId = setTimeout(() => {
897
- controller.abort();
898
- }, 1e4);
899
- const response = await fetch(
900
- `${this.config.baseUrl}/api/sdk/enterprise-flags?environment=${encodeURIComponent(environment)}`,
901
- {
902
- method: "GET",
903
- headers: {
904
- Authorization: `Bearer ${this.config.apiKey}`
905
- },
906
- signal: controller.signal
907
- }
987
+ const response = await this.fetchWithRetry(
988
+ () => {
989
+ const controller = new AbortController();
990
+ const timeoutId = setTimeout(() => {
991
+ controller.abort();
992
+ }, 1e4);
993
+ return fetch(
994
+ `${this.config.baseUrl}/api/sdk/enterprise-flags?environment=${encodeURIComponent(environment)}`,
995
+ {
996
+ method: "GET",
997
+ headers: {
998
+ Authorization: `Bearer ${this.config.apiKey}`
999
+ },
1000
+ signal: controller.signal
1001
+ }
1002
+ ).finally(() => {
1003
+ clearTimeout(timeoutId);
1004
+ });
1005
+ },
1006
+ "Get enterprise flags"
908
1007
  );
909
- clearTimeout(timeoutId);
910
- if (!response.ok) {
911
- if (response.status === 401 || response.status === 403) {
912
- this.authFailed = true;
913
- this.realtime?.disconnect();
914
- console.error(`[Savvagent] Authentication failed (${response.status}). Check your API key. Further requests disabled.`);
915
- throw new Error(`Authentication failed: ${response.status}`);
916
- }
917
- throw new Error(`Failed to fetch enterprise flags: ${response.status}`);
918
- }
919
1008
  const data = await response.json();
920
1009
  return data.flags;
921
1010
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvagent/sdk",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Savvagent TypeScript/JavaScript SDK for feature flags with AI-powered error detection",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",