@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +484 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/module.ts.html +289 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/service.ts.html +1846 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +242 -0
- package/coverage/module.ts.html +289 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/service.ts.html +1846 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/README.md +484 -0
- package/dist/esm2022/index.mjs +15 -0
- package/dist/esm2022/module.mjs +75 -0
- package/dist/esm2022/savvagent-angular.mjs +5 -0
- package/dist/esm2022/service.mjs +473 -0
- package/dist/fesm2022/savvagent-angular.mjs +563 -0
- package/dist/fesm2022/savvagent-angular.mjs.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/module.d.ts +57 -0
- package/dist/service.d.ts +319 -0
- package/jest.config.js +40 -0
- package/ng-package.json +8 -0
- package/package.json +73 -0
- package/setup-jest.ts +2 -0
- package/src/index.spec.ts +144 -0
- package/src/index.ts +38 -0
- package/src/module.spec.ts +283 -0
- package/src/module.ts +68 -0
- package/src/service.spec.ts +945 -0
- package/src/service.ts +587 -0
- package/test-utils/angular-core-mock.ts +28 -0
- package/test-utils/angular-testing-mock.ts +87 -0
- package/tsconfig.json +33 -0
- 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
|