@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
Binary file
@@ -0,0 +1,210 @@
1
+ /* eslint-disable */
2
+ var addSorting = (function() {
3
+ 'use strict';
4
+ var cols,
5
+ currentSort = {
6
+ index: 0,
7
+ desc: false
8
+ };
9
+
10
+ // returns the summary table element
11
+ function getTable() {
12
+ return document.querySelector('.coverage-summary');
13
+ }
14
+ // returns the thead element of the summary table
15
+ function getTableHeader() {
16
+ return getTable().querySelector('thead tr');
17
+ }
18
+ // returns the tbody element of the summary table
19
+ function getTableBody() {
20
+ return getTable().querySelector('tbody');
21
+ }
22
+ // returns the th element for nth column
23
+ function getNthColumn(n) {
24
+ return getTableHeader().querySelectorAll('th')[n];
25
+ }
26
+
27
+ function onFilterInput() {
28
+ const searchValue = document.getElementById('fileSearch').value;
29
+ const rows = document.getElementsByTagName('tbody')[0].children;
30
+
31
+ // Try to create a RegExp from the searchValue. If it fails (invalid regex),
32
+ // it will be treated as a plain text search
33
+ let searchRegex;
34
+ try {
35
+ searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
36
+ } catch (error) {
37
+ searchRegex = null;
38
+ }
39
+
40
+ for (let i = 0; i < rows.length; i++) {
41
+ const row = rows[i];
42
+ let isMatch = false;
43
+
44
+ if (searchRegex) {
45
+ // If a valid regex was created, use it for matching
46
+ isMatch = searchRegex.test(row.textContent);
47
+ } else {
48
+ // Otherwise, fall back to the original plain text search
49
+ isMatch = row.textContent
50
+ .toLowerCase()
51
+ .includes(searchValue.toLowerCase());
52
+ }
53
+
54
+ row.style.display = isMatch ? '' : 'none';
55
+ }
56
+ }
57
+
58
+ // loads the search box
59
+ function addSearchBox() {
60
+ var template = document.getElementById('filterTemplate');
61
+ var templateClone = template.content.cloneNode(true);
62
+ templateClone.getElementById('fileSearch').oninput = onFilterInput;
63
+ template.parentElement.appendChild(templateClone);
64
+ }
65
+
66
+ // loads all columns
67
+ function loadColumns() {
68
+ var colNodes = getTableHeader().querySelectorAll('th'),
69
+ colNode,
70
+ cols = [],
71
+ col,
72
+ i;
73
+
74
+ for (i = 0; i < colNodes.length; i += 1) {
75
+ colNode = colNodes[i];
76
+ col = {
77
+ key: colNode.getAttribute('data-col'),
78
+ sortable: !colNode.getAttribute('data-nosort'),
79
+ type: colNode.getAttribute('data-type') || 'string'
80
+ };
81
+ cols.push(col);
82
+ if (col.sortable) {
83
+ col.defaultDescSort = col.type === 'number';
84
+ colNode.innerHTML =
85
+ colNode.innerHTML + '<span class="sorter"></span>';
86
+ }
87
+ }
88
+ return cols;
89
+ }
90
+ // attaches a data attribute to every tr element with an object
91
+ // of data values keyed by column name
92
+ function loadRowData(tableRow) {
93
+ var tableCols = tableRow.querySelectorAll('td'),
94
+ colNode,
95
+ col,
96
+ data = {},
97
+ i,
98
+ val;
99
+ for (i = 0; i < tableCols.length; i += 1) {
100
+ colNode = tableCols[i];
101
+ col = cols[i];
102
+ val = colNode.getAttribute('data-value');
103
+ if (col.type === 'number') {
104
+ val = Number(val);
105
+ }
106
+ data[col.key] = val;
107
+ }
108
+ return data;
109
+ }
110
+ // loads all row data
111
+ function loadData() {
112
+ var rows = getTableBody().querySelectorAll('tr'),
113
+ i;
114
+
115
+ for (i = 0; i < rows.length; i += 1) {
116
+ rows[i].data = loadRowData(rows[i]);
117
+ }
118
+ }
119
+ // sorts the table using the data for the ith column
120
+ function sortByIndex(index, desc) {
121
+ var key = cols[index].key,
122
+ sorter = function(a, b) {
123
+ a = a.data[key];
124
+ b = b.data[key];
125
+ return a < b ? -1 : a > b ? 1 : 0;
126
+ },
127
+ finalSorter = sorter,
128
+ tableBody = document.querySelector('.coverage-summary tbody'),
129
+ rowNodes = tableBody.querySelectorAll('tr'),
130
+ rows = [],
131
+ i;
132
+
133
+ if (desc) {
134
+ finalSorter = function(a, b) {
135
+ return -1 * sorter(a, b);
136
+ };
137
+ }
138
+
139
+ for (i = 0; i < rowNodes.length; i += 1) {
140
+ rows.push(rowNodes[i]);
141
+ tableBody.removeChild(rowNodes[i]);
142
+ }
143
+
144
+ rows.sort(finalSorter);
145
+
146
+ for (i = 0; i < rows.length; i += 1) {
147
+ tableBody.appendChild(rows[i]);
148
+ }
149
+ }
150
+ // removes sort indicators for current column being sorted
151
+ function removeSortIndicators() {
152
+ var col = getNthColumn(currentSort.index),
153
+ cls = col.className;
154
+
155
+ cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
156
+ col.className = cls;
157
+ }
158
+ // adds sort indicators for current column being sorted
159
+ function addSortIndicators() {
160
+ getNthColumn(currentSort.index).className += currentSort.desc
161
+ ? ' sorted-desc'
162
+ : ' sorted';
163
+ }
164
+ // adds event listeners for all sorter widgets
165
+ function enableUI() {
166
+ var i,
167
+ el,
168
+ ithSorter = function ithSorter(i) {
169
+ var col = cols[i];
170
+
171
+ return function() {
172
+ var desc = col.defaultDescSort;
173
+
174
+ if (currentSort.index === i) {
175
+ desc = !currentSort.desc;
176
+ }
177
+ sortByIndex(i, desc);
178
+ removeSortIndicators();
179
+ currentSort.index = i;
180
+ currentSort.desc = desc;
181
+ addSortIndicators();
182
+ };
183
+ };
184
+ for (i = 0; i < cols.length; i += 1) {
185
+ if (cols[i].sortable) {
186
+ // add the click event handler on the th so users
187
+ // dont have to click on those tiny arrows
188
+ el = getNthColumn(i).querySelector('.sorter').parentElement;
189
+ if (el.addEventListener) {
190
+ el.addEventListener('click', ithSorter(i));
191
+ } else {
192
+ el.attachEvent('onclick', ithSorter(i));
193
+ }
194
+ }
195
+ }
196
+ }
197
+ // adds sorting functionality to the UI
198
+ return function() {
199
+ if (!getTable()) {
200
+ return;
201
+ }
202
+ cols = loadColumns();
203
+ loadData();
204
+ addSearchBox();
205
+ addSortIndicators();
206
+ enableUI();
207
+ };
208
+ })();
209
+
210
+ window.addEventListener('load', addSorting);
package/dist/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
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @savvagent/angular - Angular SDK for Savvagent feature flags
3
+ *
4
+ * This package provides Angular services and modules for easy integration
5
+ * of Savvagent feature flags into Angular applications.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ // Module
10
+ export { SavvagentModule } from './module';
11
+ // Service and types
12
+ export { SavvagentService, SAVVAGENT_CONFIG } from './service';
13
+ // Re-export FlagClient for advanced use cases
14
+ export { FlagClient } from '@savvagent/sdk';
15
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7R0FPRztBQUVILFNBQVM7QUFDVCxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBRTNDLG9CQUFvQjtBQUNwQixPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxXQUFXLENBQUM7QUF1Qi9ELDhDQUE4QztBQUM5QyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sZ0JBQWdCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIEBzYXZ2YWdlbnQvYW5ndWxhciAtIEFuZ3VsYXIgU0RLIGZvciBTYXZ2YWdlbnQgZmVhdHVyZSBmbGFnc1xuICpcbiAqIFRoaXMgcGFja2FnZSBwcm92aWRlcyBBbmd1bGFyIHNlcnZpY2VzIGFuZCBtb2R1bGVzIGZvciBlYXN5IGludGVncmF0aW9uXG4gKiBvZiBTYXZ2YWdlbnQgZmVhdHVyZSBmbGFncyBpbnRvIEFuZ3VsYXIgYXBwbGljYXRpb25zLlxuICpcbiAqIEBwYWNrYWdlRG9jdW1lbnRhdGlvblxuICovXG5cbi8vIE1vZHVsZVxuZXhwb3J0IHsgU2F2dmFnZW50TW9kdWxlIH0gZnJvbSAnLi9tb2R1bGUnO1xuXG4vLyBTZXJ2aWNlIGFuZCB0eXBlc1xuZXhwb3J0IHsgU2F2dmFnZW50U2VydmljZSwgU0FWVkFHRU5UX0NPTkZJRyB9IGZyb20gJy4vc2VydmljZSc7XG5leHBvcnQgdHlwZSB7XG4gIFNhdnZhZ2VudENvbmZpZyxcbiAgRGVmYXVsdEZsYWdDb250ZXh0LFxuICBGbGFnT2JzZXJ2YWJsZVJlc3VsdCxcbiAgRmxhZ09wdGlvbnMsXG59IGZyb20gJy4vc2VydmljZSc7XG5cbi8vIFJlLWV4cG9ydCB0eXBlcyBmcm9tIGNvcmUgU0RLXG5leHBvcnQgdHlwZSB7XG4gIEZsYWdDbGllbnRDb25maWcsXG4gIEZsYWdDb250ZXh0LFxuICBGbGFnRXZhbHVhdGlvblJlc3VsdCxcbiAgRXZhbHVhdGlvbkV2ZW50LFxuICBFcnJvckV2ZW50LFxuICBGbGFnVXBkYXRlRXZlbnQsXG4gIEZsYWdEZWZpbml0aW9uLFxuICBGbGFnTGlzdFJlc3BvbnNlLFxuICAvLyBHZW5lcmF0ZWQgQVBJIHR5cGVzIGZvciBhZHZhbmNlZCB1c2Vyc1xuICBBcGlUeXBlcyxcbiAgY29tcG9uZW50cyxcbn0gZnJvbSAnQHNhdnZhZ2VudC9zZGsnO1xuXG4vLyBSZS1leHBvcnQgRmxhZ0NsaWVudCBmb3IgYWR2YW5jZWQgdXNlIGNhc2VzXG5leHBvcnQgeyBGbGFnQ2xpZW50IH0gZnJvbSAnQHNhdnZhZ2VudC9zZGsnO1xuIl19