@savvagent/angular 1.0.1

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +484 -0
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +131 -0
  8. package/coverage/lcov-report/base.css +224 -0
  9. package/coverage/lcov-report/block-navigation.js +87 -0
  10. package/coverage/lcov-report/favicon.png +0 -0
  11. package/coverage/lcov-report/index.html +131 -0
  12. package/coverage/lcov-report/module.ts.html +289 -0
  13. package/coverage/lcov-report/prettify.css +1 -0
  14. package/coverage/lcov-report/prettify.js +2 -0
  15. package/coverage/lcov-report/service.ts.html +1846 -0
  16. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  17. package/coverage/lcov-report/sorter.js +210 -0
  18. package/coverage/lcov.info +242 -0
  19. package/coverage/module.ts.html +289 -0
  20. package/coverage/prettify.css +1 -0
  21. package/coverage/prettify.js +2 -0
  22. package/coverage/service.ts.html +1846 -0
  23. package/coverage/sort-arrow-sprite.png +0 -0
  24. package/coverage/sorter.js +210 -0
  25. package/dist/README.md +484 -0
  26. package/dist/esm2022/index.mjs +15 -0
  27. package/dist/esm2022/module.mjs +75 -0
  28. package/dist/esm2022/savvagent-angular.mjs +5 -0
  29. package/dist/esm2022/service.mjs +473 -0
  30. package/dist/fesm2022/savvagent-angular.mjs +563 -0
  31. package/dist/fesm2022/savvagent-angular.mjs.map +1 -0
  32. package/dist/index.d.ts +13 -0
  33. package/dist/module.d.ts +57 -0
  34. package/dist/service.d.ts +319 -0
  35. package/jest.config.js +40 -0
  36. package/ng-package.json +8 -0
  37. package/package.json +73 -0
  38. package/setup-jest.ts +2 -0
  39. package/src/index.spec.ts +144 -0
  40. package/src/index.ts +38 -0
  41. package/src/module.spec.ts +283 -0
  42. package/src/module.ts +68 -0
  43. package/src/service.spec.ts +945 -0
  44. package/src/service.ts +587 -0
  45. package/test-utils/angular-core-mock.ts +28 -0
  46. package/test-utils/angular-testing-mock.ts +87 -0
  47. package/tsconfig.json +33 -0
  48. package/tsconfig.spec.json +11 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # @savvagent/angular
2
+
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updates for new API parameters
8
+ - Updated dependencies
9
+ - @savvagent/sdk@1.0.1
10
+
11
+ ## 1.0.0
12
+
13
+ ### Features
14
+
15
+ - Initial release of Angular SDK for Savvagent feature flags
16
+ - `SavvagentModule` - Angular module for easy SDK configuration with `forRoot()`
17
+ - `SavvagentService` - Injectable service with full feature flag functionality
18
+ - Reactive API with RxJS Observables (`flag$`, `flagValue$`, `getAllFlags$`)
19
+ - Promise-based API for non-reactive use cases (`evaluate`, `isEnabled`, `withFlag`)
20
+ - Real-time flag updates via SSE subscription
21
+ - Local override support for development and testing
22
+ - User identification and anonymous ID management
23
+ - Error tracking with flag context for AI-powered analysis
24
+ - Full TypeScript support with comprehensive type definitions
25
+ - Support for Angular 14+ (including standalone components)
26
+ - Default context configuration for application-wide settings
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Savvagent, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,484 @@
1
+ # @savvagent/angular
2
+
3
+ Angular SDK for Savvagent - AI-powered feature flags that prevent production incidents.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @savvagent/angular
9
+ # or
10
+ pnpm add @savvagent/angular
11
+ # or
12
+ yarn add @savvagent/angular
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Import SavvagentModule in your app
18
+
19
+ ```typescript
20
+ // app.module.ts
21
+ import { SavvagentModule } from '@savvagent/angular';
22
+
23
+ @NgModule({
24
+ imports: [
25
+ SavvagentModule.forRoot({
26
+ config: {
27
+ apiKey: 'sdk_your_api_key_here',
28
+ applicationId: 'your-app-id', // Optional: for application-scoped flags
29
+ enableRealtime: true, // Enable real-time flag updates
30
+ },
31
+ defaultContext: {
32
+ environment: 'production',
33
+ userId: 'user-123', // Optional: set default user
34
+ }
35
+ })
36
+ ]
37
+ })
38
+ export class AppModule {}
39
+ ```
40
+
41
+ ### 2. Use SavvagentService in your components
42
+
43
+ ```typescript
44
+ import { Component } from '@angular/core';
45
+ import { SavvagentService } from '@savvagent/angular';
46
+
47
+ @Component({
48
+ selector: 'app-my-feature',
49
+ template: `
50
+ <ng-container *ngIf="newFeature$ | async as flag">
51
+ <app-spinner *ngIf="flag.loading"></app-spinner>
52
+ <app-new-checkout *ngIf="flag.value"></app-new-checkout>
53
+ <app-old-checkout *ngIf="!flag.value && !flag.loading"></app-old-checkout>
54
+ </ng-container>
55
+ `
56
+ })
57
+ export class MyFeatureComponent {
58
+ newFeature$ = this.savvagent.flag$('new-checkout-flow', {
59
+ defaultValue: false,
60
+ realtime: true,
61
+ });
62
+
63
+ constructor(private savvagent: SavvagentService) {}
64
+ }
65
+ ```
66
+
67
+ ## Standalone Components (Angular 14+)
68
+
69
+ For standalone components, you can use `importProvidersFrom`:
70
+
71
+ ```typescript
72
+ // main.ts
73
+ import { bootstrapApplication } from '@angular/platform-browser';
74
+ import { importProvidersFrom } from '@angular/core';
75
+ import { SavvagentModule } from '@savvagent/angular';
76
+ import { AppComponent } from './app/app.component';
77
+
78
+ bootstrapApplication(AppComponent, {
79
+ providers: [
80
+ importProvidersFrom(
81
+ SavvagentModule.forRoot({
82
+ config: { apiKey: 'sdk_your_api_key' }
83
+ })
84
+ )
85
+ ]
86
+ });
87
+ ```
88
+
89
+ ## API Reference
90
+
91
+ ### `SavvagentModule`
92
+
93
+ Angular module that configures the Savvagent SDK.
94
+
95
+ #### `SavvagentModule.forRoot(config)`
96
+
97
+ Configure the module with your API key and default context.
98
+
99
+ ```typescript
100
+ interface SavvagentConfig {
101
+ config: FlagClientConfig;
102
+ defaultContext?: DefaultFlagContext;
103
+ }
104
+
105
+ interface FlagClientConfig {
106
+ /** SDK API key (starts with sdk_) */
107
+ apiKey: string;
108
+ /** Application ID for application-scoped flags */
109
+ applicationId?: string;
110
+ /** Base URL for the Savvagent API */
111
+ baseUrl?: string;
112
+ /** Enable real-time flag updates via SSE (default: true) */
113
+ enableRealtime?: boolean;
114
+ /** Cache TTL in milliseconds (default: 60000) */
115
+ cacheTtl?: number;
116
+ /** Enable telemetry tracking (default: true) */
117
+ enableTelemetry?: boolean;
118
+ /** Default flag values when evaluation fails */
119
+ defaults?: Record<string, boolean>;
120
+ /** Custom error handler */
121
+ onError?: (error: Error) => void;
122
+ }
123
+
124
+ interface DefaultFlagContext {
125
+ applicationId?: string;
126
+ environment?: string;
127
+ organizationId?: string;
128
+ userId?: string;
129
+ anonymousId?: string;
130
+ sessionId?: string;
131
+ language?: string;
132
+ attributes?: Record<string, any>;
133
+ }
134
+ ```
135
+
136
+ ### `SavvagentService`
137
+
138
+ Injectable service that provides all feature flag functionality.
139
+
140
+ #### Properties
141
+
142
+ - `ready$: Observable<boolean>` - Observable that emits true when the client is ready
143
+ - `isReady: boolean` - Check if the client is ready synchronously
144
+ - `flagClient: FlagClient | null` - Access the underlying FlagClient for advanced use cases
145
+
146
+ #### `flag$(flagKey, options)`
147
+
148
+ Get a reactive Observable for a feature flag with automatic updates.
149
+
150
+ ```typescript
151
+ interface FlagOptions {
152
+ /** Context for flag evaluation (user_id, attributes, etc.) */
153
+ context?: FlagContext;
154
+ /** Default value to use while loading or on error */
155
+ defaultValue?: boolean;
156
+ /** Enable real-time updates for this flag (default: true) */
157
+ realtime?: boolean;
158
+ }
159
+
160
+ interface FlagObservableResult {
161
+ /** Current flag value */
162
+ value: boolean;
163
+ /** Whether the flag is currently being evaluated */
164
+ loading: boolean;
165
+ /** Error if evaluation failed */
166
+ error: Error | null;
167
+ /** Detailed evaluation result */
168
+ result: FlagEvaluationResult | null;
169
+ }
170
+ ```
171
+
172
+ **Example:**
173
+
174
+ ```typescript
175
+ @Component({
176
+ template: `
177
+ <ng-container *ngIf="betaFeature$ | async as flag">
178
+ <div *ngIf="flag.loading">Loading...</div>
179
+ <div *ngIf="flag.error">Error: {{ flag.error.message }}</div>
180
+ <app-beta *ngIf="flag.value"></app-beta>
181
+ <app-standard *ngIf="!flag.value && !flag.loading"></app-standard>
182
+ </ng-container>
183
+ `
184
+ })
185
+ export class MyComponent {
186
+ betaFeature$ = this.savvagent.flag$('beta-feature', {
187
+ context: {
188
+ user_id: this.userId,
189
+ attributes: { plan: 'pro' }
190
+ },
191
+ defaultValue: false,
192
+ realtime: true
193
+ });
194
+
195
+ constructor(private savvagent: SavvagentService) {}
196
+ }
197
+ ```
198
+
199
+ #### `flagValue$(flagKey, options)`
200
+
201
+ Get just the boolean value as an Observable. Useful when you don't need loading/error states.
202
+
203
+ ```typescript
204
+ @Component({
205
+ template: `
206
+ <button *ngIf="isFeatureEnabled$ | async">New Button</button>
207
+ `
208
+ })
209
+ export class SimpleComponent {
210
+ isFeatureEnabled$ = this.savvagent.flagValue$('my-feature');
211
+
212
+ constructor(private savvagent: SavvagentService) {}
213
+ }
214
+ ```
215
+
216
+ #### `evaluate(flagKey, context)`
217
+
218
+ Evaluate a feature flag once (non-reactive).
219
+
220
+ ```typescript
221
+ async checkFeature() {
222
+ const result = await this.savvagent.evaluate('new-feature');
223
+ console.log(result.value, result.reason);
224
+ }
225
+ ```
226
+
227
+ #### `isEnabled(flagKey, context)`
228
+
229
+ Simple boolean check if a flag is enabled.
230
+
231
+ ```typescript
232
+ async doSomething() {
233
+ if (await this.savvagent.isEnabled('feature-flag')) {
234
+ // Feature is enabled
235
+ }
236
+ }
237
+ ```
238
+
239
+ #### `withFlag(flagKey, callback, context)`
240
+
241
+ Execute code conditionally based on flag value.
242
+
243
+ ```typescript
244
+ async trackPageView() {
245
+ await this.savvagent.withFlag('analytics-enabled', async () => {
246
+ await this.analytics.track('page_view');
247
+ });
248
+ }
249
+ ```
250
+
251
+ #### `trackError(flagKey, error, context)`
252
+
253
+ Track errors with flag context for AI-powered analysis.
254
+
255
+ ```typescript
256
+ handleError(error: Error) {
257
+ this.savvagent.trackError('new-payment-flow', error);
258
+ }
259
+ ```
260
+
261
+ #### User Management
262
+
263
+ ```typescript
264
+ // Set user ID for logged-in users
265
+ setUserId(userId: string | null): void;
266
+ getUserId(): string | null;
267
+
268
+ // Anonymous ID management
269
+ getAnonymousId(): string | null;
270
+ setAnonymousId(id: string): void;
271
+ ```
272
+
273
+ #### Local Overrides
274
+
275
+ For development and testing:
276
+
277
+ ```typescript
278
+ // Set override (takes precedence over server values)
279
+ setOverride(flagKey: string, value: boolean): void;
280
+
281
+ // Clear overrides
282
+ clearOverride(flagKey: string): void;
283
+ clearAllOverrides(): void;
284
+
285
+ // Check overrides
286
+ hasOverride(flagKey: string): boolean;
287
+ getOverride(flagKey: string): boolean | undefined;
288
+ getOverrides(): Record<string, boolean>;
289
+
290
+ // Set multiple overrides
291
+ setOverrides(overrides: Record<string, boolean>): void;
292
+ ```
293
+
294
+ #### Flag Discovery
295
+
296
+ ```typescript
297
+ // Get all flags (returns Observable)
298
+ getAllFlags$(environment?: string): Observable<FlagDefinition[]>;
299
+
300
+ // Get all flags (Promise-based)
301
+ getAllFlags(environment?: string): Promise<FlagDefinition[]>;
302
+
303
+ // Get enterprise-scoped flags only
304
+ getEnterpriseFlags(environment?: string): Promise<FlagDefinition[]>;
305
+ ```
306
+
307
+ #### Cache & Connection
308
+
309
+ ```typescript
310
+ clearCache(): void;
311
+ isRealtimeConnected(): boolean;
312
+ close(): void;
313
+ ```
314
+
315
+ ## Advanced Examples
316
+
317
+ ### User Targeting
318
+
319
+ ```typescript
320
+ @Component({...})
321
+ export class UserFeatureComponent implements OnInit {
322
+ premiumFeature$!: Observable<FlagObservableResult>;
323
+
324
+ constructor(
325
+ private savvagent: SavvagentService,
326
+ private auth: AuthService
327
+ ) {}
328
+
329
+ ngOnInit() {
330
+ this.premiumFeature$ = this.savvagent.flag$('premium-features', {
331
+ context: {
332
+ user_id: this.auth.userId,
333
+ attributes: {
334
+ plan: this.auth.userPlan,
335
+ signupDate: this.auth.signupDate
336
+ }
337
+ }
338
+ });
339
+ }
340
+ }
341
+ ```
342
+
343
+ ### Dynamic Initialization
344
+
345
+ If you need to initialize the service after getting user data:
346
+
347
+ ```typescript
348
+ @Component({...})
349
+ export class AppComponent implements OnInit {
350
+ constructor(
351
+ private savvagent: SavvagentService,
352
+ private auth: AuthService
353
+ ) {}
354
+
355
+ ngOnInit() {
356
+ // Wait for auth, then initialize
357
+ this.auth.user$.pipe(take(1)).subscribe(user => {
358
+ this.savvagent.initialize({
359
+ config: {
360
+ apiKey: environment.savvagentApiKey
361
+ },
362
+ defaultContext: {
363
+ userId: user?.id,
364
+ environment: environment.name
365
+ }
366
+ });
367
+ });
368
+ }
369
+ }
370
+ ```
371
+
372
+ ### Error Tracking
373
+
374
+ ```typescript
375
+ @Component({...})
376
+ export class PaymentComponent {
377
+ constructor(private savvagent: SavvagentService) {}
378
+
379
+ async processPayment() {
380
+ try {
381
+ const result = await this.paymentService.process();
382
+ return result;
383
+ } catch (error) {
384
+ // Error is correlated with flag changes
385
+ this.savvagent.trackError('new-payment-flow', error as Error);
386
+ throw error;
387
+ }
388
+ }
389
+ }
390
+ ```
391
+
392
+ ### A/B Testing
393
+
394
+ ```typescript
395
+ @Component({
396
+ template: `
397
+ <app-checkout-a *ngIf="!(variantB$ | async)"></app-checkout-a>
398
+ <app-checkout-b *ngIf="variantB$ | async"></app-checkout-b>
399
+ `
400
+ })
401
+ export class ABTestComponent {
402
+ variantB$ = this.savvagent.flagValue$('checkout-variant-b', {
403
+ context: {
404
+ user_id: this.userId // Consistent assignment per user
405
+ }
406
+ });
407
+
408
+ constructor(private savvagent: SavvagentService) {}
409
+ }
410
+ ```
411
+
412
+ ### Development Override Panel
413
+
414
+ ```typescript
415
+ @Component({
416
+ selector: 'app-flag-overrides',
417
+ template: `
418
+ <div *ngFor="let flag of flags$ | async">
419
+ <label>
420
+ <input
421
+ type="checkbox"
422
+ [checked]="savvagent.getOverride(flag.key) ?? flag.enabled"
423
+ (change)="toggleOverride(flag.key, $event)"
424
+ />
425
+ {{ flag.key }}
426
+ </label>
427
+ <button (click)="clearOverride(flag.key)">Reset</button>
428
+ </div>
429
+ `
430
+ })
431
+ export class FlagOverridesComponent implements OnInit {
432
+ flags$ = this.savvagent.getAllFlags$('development');
433
+
434
+ constructor(public savvagent: SavvagentService) {}
435
+
436
+ toggleOverride(flagKey: string, event: Event) {
437
+ const checked = (event.target as HTMLInputElement).checked;
438
+ this.savvagent.setOverride(flagKey, checked);
439
+ }
440
+
441
+ clearOverride(flagKey: string) {
442
+ this.savvagent.clearOverride(flagKey);
443
+ }
444
+ }
445
+ ```
446
+
447
+ ## TypeScript Support
448
+
449
+ This package is written in TypeScript and provides full type definitions.
450
+
451
+ ```typescript
452
+ import type {
453
+ FlagClientConfig,
454
+ FlagContext,
455
+ FlagEvaluationResult,
456
+ FlagDefinition,
457
+ SavvagentConfig,
458
+ DefaultFlagContext,
459
+ FlagObservableResult,
460
+ FlagOptions,
461
+ } from '@savvagent/angular';
462
+ ```
463
+
464
+ ## Best Practices
465
+
466
+ 1. **Import SavvagentModule.forRoot() in your root module** to ensure a single instance of the service.
467
+
468
+ 2. **Use the `defaultValue` option** to provide a safe fallback while flags are loading.
469
+
470
+ 3. **Enable real-time updates** for flags that change frequently or require immediate propagation.
471
+
472
+ 4. **Track errors** in new features to leverage Savvagent's AI-powered error correlation.
473
+
474
+ 5. **Use user context** for targeted rollouts based on user attributes, location, or behavior.
475
+
476
+ 6. **Handle loading states** gracefully using the async pipe and conditional rendering.
477
+
478
+ 7. **Use `flagValue$`** when you only need the boolean value without loading/error states.
479
+
480
+ 8. **Clean up subscriptions** - the service handles cleanup automatically on destroy, but use `takeUntil` or similar patterns in components for long-lived subscriptions.
481
+
482
+ ## License
483
+
484
+ MIT