@setgo/sdk 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/README.md +184 -0
- package/dist/core/client.d.ts +81 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +319 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +14 -0
- package/dist/core/metrics-reporter.d.ts +40 -0
- package/dist/core/metrics-reporter.d.ts.map +1 -0
- package/dist/core/metrics-reporter.js +152 -0
- package/dist/core/repository.d.ts +37 -0
- package/dist/core/repository.d.ts.map +1 -0
- package/dist/core/repository.js +58 -0
- package/dist/core/strategy-evaluator.d.ts +43 -0
- package/dist/core/strategy-evaluator.d.ts.map +1 -0
- package/dist/core/strategy-evaluator.js +209 -0
- package/dist/errors/index.d.ts +22 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +52 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/nestjs/index.d.ts +7 -0
- package/dist/nestjs/index.d.ts.map +1 -0
- package/dist/nestjs/index.js +13 -0
- package/dist/nestjs/setgo.constants.d.ts +6 -0
- package/dist/nestjs/setgo.constants.d.ts.map +1 -0
- package/dist/nestjs/setgo.constants.js +8 -0
- package/dist/nestjs/setgo.module.d.ts +20 -0
- package/dist/nestjs/setgo.module.d.ts.map +1 -0
- package/dist/nestjs/setgo.module.js +64 -0
- package/dist/nestjs/setgo.service.d.ts +34 -0
- package/dist/nestjs/setgo.service.d.ts.map +1 -0
- package/dist/nestjs/setgo.service.js +84 -0
- package/dist/types/config.d.ts +41 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +5 -0
- package/dist/types/events.d.ts +21 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +5 -0
- package/dist/types/features.d.ts +41 -0
- package/dist/types/features.d.ts.map +1 -0
- package/dist/types/features.js +5 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/metrics.d.ts +16 -0
- package/dist/types/metrics.d.ts.map +1 -0
- package/dist/types/metrics.js +5 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/ip-matcher.d.ts +14 -0
- package/dist/utils/ip-matcher.d.ts.map +1 -0
- package/dist/utils/ip-matcher.js +165 -0
- package/dist/utils/murmurhash.d.ts +10 -0
- package/dist/utils/murmurhash.d.ts.map +1 -0
- package/dist/utils/murmurhash.js +59 -0
- package/package.json +72 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Metrics Reporter - collects and sends usage metrics
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MetricsReporter = void 0;
|
|
7
|
+
const index_js_1 = require("../errors/index.js");
|
|
8
|
+
class MetricsReporter {
|
|
9
|
+
metrics = new Map();
|
|
10
|
+
bucketStart;
|
|
11
|
+
sendInterval;
|
|
12
|
+
config;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.bucketStart = this.getCurrentHourBucket();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the current hour bucket timestamp
|
|
19
|
+
*/
|
|
20
|
+
getCurrentHourBucket() {
|
|
21
|
+
const now = new Date();
|
|
22
|
+
now.setMinutes(0, 0, 0);
|
|
23
|
+
return now;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start the metrics reporting interval
|
|
27
|
+
*/
|
|
28
|
+
start() {
|
|
29
|
+
if (this.config.disableMetrics)
|
|
30
|
+
return;
|
|
31
|
+
this.sendInterval = setInterval(() => {
|
|
32
|
+
void this.flush();
|
|
33
|
+
}, this.config.metricsInterval);
|
|
34
|
+
// Ensure the interval doesn't prevent Node.js from exiting
|
|
35
|
+
if (this.sendInterval.unref) {
|
|
36
|
+
this.sendInterval.unref();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Stop the metrics reporting interval
|
|
41
|
+
*/
|
|
42
|
+
stop() {
|
|
43
|
+
if (this.sendInterval) {
|
|
44
|
+
clearInterval(this.sendInterval);
|
|
45
|
+
this.sendInterval = undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Track a feature evaluation
|
|
50
|
+
*/
|
|
51
|
+
track(featureName, enabled) {
|
|
52
|
+
if (this.config.disableMetrics)
|
|
53
|
+
return;
|
|
54
|
+
// Check if we need to rotate the bucket
|
|
55
|
+
const currentBucket = this.getCurrentHourBucket();
|
|
56
|
+
if (currentBucket.getTime() !== this.bucketStart.getTime()) {
|
|
57
|
+
// New hour, flush old metrics and start fresh
|
|
58
|
+
void this.flush();
|
|
59
|
+
this.bucketStart = currentBucket;
|
|
60
|
+
}
|
|
61
|
+
const existing = this.metrics.get(featureName) ?? { yes: 0, no: 0 };
|
|
62
|
+
if (enabled) {
|
|
63
|
+
existing.yes++;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
existing.no++;
|
|
67
|
+
}
|
|
68
|
+
this.metrics.set(featureName, existing);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Flush metrics to the server
|
|
72
|
+
*/
|
|
73
|
+
async flush() {
|
|
74
|
+
if (this.config.disableMetrics)
|
|
75
|
+
return;
|
|
76
|
+
if (this.metrics.size === 0)
|
|
77
|
+
return;
|
|
78
|
+
const toggles = [];
|
|
79
|
+
for (const [featureName, data] of this.metrics.entries()) {
|
|
80
|
+
if (data.yes > 0 || data.no > 0) {
|
|
81
|
+
toggles.push({
|
|
82
|
+
featureName,
|
|
83
|
+
yes: data.yes,
|
|
84
|
+
no: data.no,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (toggles.length === 0)
|
|
89
|
+
return;
|
|
90
|
+
const payload = {
|
|
91
|
+
appName: this.config.appName,
|
|
92
|
+
instanceId: this.config.instanceId,
|
|
93
|
+
bucket: this.bucketStart.toISOString(),
|
|
94
|
+
toggles,
|
|
95
|
+
};
|
|
96
|
+
// Clear metrics before sending (to avoid losing new metrics during send)
|
|
97
|
+
this.metrics.clear();
|
|
98
|
+
try {
|
|
99
|
+
await this.sendMetrics(payload);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Re-add metrics on failure (best effort)
|
|
103
|
+
for (const toggle of toggles) {
|
|
104
|
+
const existing = this.metrics.get(toggle.featureName) ?? { yes: 0, no: 0 };
|
|
105
|
+
existing.yes += toggle.yes;
|
|
106
|
+
existing.no += toggle.no;
|
|
107
|
+
this.metrics.set(toggle.featureName, existing);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Send metrics to the API
|
|
113
|
+
*/
|
|
114
|
+
async sendMetrics(payload) {
|
|
115
|
+
const url = `${this.config.baseUrl}/client/metrics`;
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(url, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
Authorization: this.config.token,
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify(payload),
|
|
126
|
+
signal: controller.signal,
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new index_js_1.NetworkError(`Failed to send metrics: ${response.statusText}`, response.status);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
134
|
+
throw new index_js_1.TimeoutError('Metrics request timed out');
|
|
135
|
+
}
|
|
136
|
+
if (error instanceof index_js_1.NetworkError) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
throw new index_js_1.NetworkError(`Failed to send metrics: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, error instanceof Error ? error : undefined);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
clearTimeout(timeoutId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get current metrics count (for testing/debugging)
|
|
147
|
+
*/
|
|
148
|
+
getMetricsCount() {
|
|
149
|
+
return this.metrics.size;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.MetricsReporter = MetricsReporter;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory feature repository
|
|
3
|
+
*/
|
|
4
|
+
import type { Feature, FeaturesResponse } from '../types/index.js';
|
|
5
|
+
export declare class FeatureRepository {
|
|
6
|
+
private features;
|
|
7
|
+
private version;
|
|
8
|
+
/**
|
|
9
|
+
* Update the repository with new features
|
|
10
|
+
*/
|
|
11
|
+
update(response: FeaturesResponse): void;
|
|
12
|
+
/**
|
|
13
|
+
* Get a specific feature by name
|
|
14
|
+
*/
|
|
15
|
+
get(name: string): Feature | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Get all features
|
|
18
|
+
*/
|
|
19
|
+
getAll(): Feature[];
|
|
20
|
+
/**
|
|
21
|
+
* Check if a feature exists
|
|
22
|
+
*/
|
|
23
|
+
has(name: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Get current version
|
|
26
|
+
*/
|
|
27
|
+
getVersion(): number;
|
|
28
|
+
/**
|
|
29
|
+
* Get the number of features
|
|
30
|
+
*/
|
|
31
|
+
size(): number;
|
|
32
|
+
/**
|
|
33
|
+
* Clear all features
|
|
34
|
+
*/
|
|
35
|
+
clear(): void;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/core/repository.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAEnE,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAmC;IACnD,OAAO,CAAC,OAAO,CAAa;IAE5B;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IASxC;;OAEG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAItC;;OAEG;IACH,MAAM,IAAI,OAAO,EAAE;IAInB;;OAEG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B;;OAEG;IACH,UAAU,IAAI,MAAM;IAIpB;;OAEG;IACH,IAAI,IAAI,MAAM;IAId;;OAEG;IACH,KAAK,IAAI,IAAI;CAId"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory feature repository
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.FeatureRepository = void 0;
|
|
7
|
+
class FeatureRepository {
|
|
8
|
+
features = new Map();
|
|
9
|
+
version = 0;
|
|
10
|
+
/**
|
|
11
|
+
* Update the repository with new features
|
|
12
|
+
*/
|
|
13
|
+
update(response) {
|
|
14
|
+
this.version = response.version;
|
|
15
|
+
this.features.clear();
|
|
16
|
+
for (const feature of response.features) {
|
|
17
|
+
this.features.set(feature.name, feature);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get a specific feature by name
|
|
22
|
+
*/
|
|
23
|
+
get(name) {
|
|
24
|
+
return this.features.get(name);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get all features
|
|
28
|
+
*/
|
|
29
|
+
getAll() {
|
|
30
|
+
return Array.from(this.features.values());
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if a feature exists
|
|
34
|
+
*/
|
|
35
|
+
has(name) {
|
|
36
|
+
return this.features.has(name);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get current version
|
|
40
|
+
*/
|
|
41
|
+
getVersion() {
|
|
42
|
+
return this.version;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get the number of features
|
|
46
|
+
*/
|
|
47
|
+
size() {
|
|
48
|
+
return this.features.size;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Clear all features
|
|
52
|
+
*/
|
|
53
|
+
clear() {
|
|
54
|
+
this.features.clear();
|
|
55
|
+
this.version = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.FeatureRepository = FeatureRepository;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy Evaluator - evaluates feature flag strategies locally
|
|
3
|
+
*/
|
|
4
|
+
import type { Strategy, FeatureContext } from '../types/index.js';
|
|
5
|
+
export declare class StrategyEvaluator {
|
|
6
|
+
/**
|
|
7
|
+
* Evaluate a single strategy
|
|
8
|
+
*/
|
|
9
|
+
evaluate(strategy: Strategy, context?: FeatureContext): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* DEFAULT strategy - returns enabled status
|
|
12
|
+
*/
|
|
13
|
+
private evaluateDefault;
|
|
14
|
+
/**
|
|
15
|
+
* GRADUAL_ROLLOUT - percentage-based rollout using hash
|
|
16
|
+
*/
|
|
17
|
+
private evaluateGradualRollout;
|
|
18
|
+
/**
|
|
19
|
+
* USER_WITH_ID - check if userId is in the allowed list
|
|
20
|
+
*/
|
|
21
|
+
private evaluateUserWithId;
|
|
22
|
+
/**
|
|
23
|
+
* FLEXIBLE_ROLLOUT - percentage with custom stickiness
|
|
24
|
+
*/
|
|
25
|
+
private evaluateFlexibleRollout;
|
|
26
|
+
/**
|
|
27
|
+
* REMOTE_ADDRESS - check if IP matches allowed IPs/CIDRs
|
|
28
|
+
*/
|
|
29
|
+
private evaluateRemoteAddress;
|
|
30
|
+
/**
|
|
31
|
+
* Check if context satisfies all constraints
|
|
32
|
+
*/
|
|
33
|
+
private checkConstraints;
|
|
34
|
+
/**
|
|
35
|
+
* Check a single constraint
|
|
36
|
+
*/
|
|
37
|
+
private checkConstraint;
|
|
38
|
+
/**
|
|
39
|
+
* Get value from context by name
|
|
40
|
+
*/
|
|
41
|
+
private getContextValue;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=strategy-evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy-evaluator.d.ts","sourceRoot":"","sources":["../../src/core/strategy-evaluator.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAIlE,qBAAa,iBAAiB;IAC5B;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO;IAiCnE;;OAEG;IACH,OAAO,CAAC,eAAe;IAIvB;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAuC9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAe1B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAqC/B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAe7B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAqDvB;;OAEG;IACH,OAAO,CAAC,eAAe;CAYxB"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Strategy Evaluator - evaluates feature flag strategies locally
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.StrategyEvaluator = void 0;
|
|
7
|
+
const murmurhash_js_1 = require("../utils/murmurhash.js");
|
|
8
|
+
const ip_matcher_js_1 = require("../utils/ip-matcher.js");
|
|
9
|
+
class StrategyEvaluator {
|
|
10
|
+
/**
|
|
11
|
+
* Evaluate a single strategy
|
|
12
|
+
*/
|
|
13
|
+
evaluate(strategy, context = {}) {
|
|
14
|
+
if (!strategy.enabled) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
// Check constraints first (if any)
|
|
18
|
+
if (strategy.constraints && strategy.constraints.length > 0) {
|
|
19
|
+
if (!this.checkConstraints(strategy.constraints, context)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
switch (strategy.type) {
|
|
24
|
+
case 'DEFAULT':
|
|
25
|
+
return this.evaluateDefault(strategy);
|
|
26
|
+
case 'GRADUAL_ROLLOUT':
|
|
27
|
+
return this.evaluateGradualRollout(strategy, context);
|
|
28
|
+
case 'USER_WITH_ID':
|
|
29
|
+
return this.evaluateUserWithId(strategy, context);
|
|
30
|
+
case 'FLEXIBLE_ROLLOUT':
|
|
31
|
+
return this.evaluateFlexibleRollout(strategy, context);
|
|
32
|
+
case 'REMOTE_ADDRESS':
|
|
33
|
+
return this.evaluateRemoteAddress(strategy, context);
|
|
34
|
+
default:
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* DEFAULT strategy - returns enabled status
|
|
40
|
+
*/
|
|
41
|
+
evaluateDefault(strategy) {
|
|
42
|
+
return strategy.enabled;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* GRADUAL_ROLLOUT - percentage-based rollout using hash
|
|
46
|
+
*/
|
|
47
|
+
evaluateGradualRollout(strategy, context) {
|
|
48
|
+
const params = strategy.parameters;
|
|
49
|
+
const percentage = params?.percentage ?? 0;
|
|
50
|
+
if (percentage <= 0)
|
|
51
|
+
return false;
|
|
52
|
+
if (percentage >= 100)
|
|
53
|
+
return true;
|
|
54
|
+
const stickiness = params?.stickiness ?? 'userId';
|
|
55
|
+
const groupId = params?.groupId ?? strategy.name;
|
|
56
|
+
let identifier;
|
|
57
|
+
switch (stickiness) {
|
|
58
|
+
case 'userId':
|
|
59
|
+
identifier = context.userId;
|
|
60
|
+
break;
|
|
61
|
+
case 'sessionId':
|
|
62
|
+
identifier = context.sessionId;
|
|
63
|
+
break;
|
|
64
|
+
case 'random':
|
|
65
|
+
identifier = Math.random().toString();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (!identifier) {
|
|
69
|
+
// If no identifier available, use random for this evaluation
|
|
70
|
+
identifier = Math.random().toString();
|
|
71
|
+
}
|
|
72
|
+
const hash = (0, murmurhash_js_1.normalizedHash)(groupId, identifier, 100);
|
|
73
|
+
return hash <= percentage;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* USER_WITH_ID - check if userId is in the allowed list
|
|
77
|
+
*/
|
|
78
|
+
evaluateUserWithId(strategy, context) {
|
|
79
|
+
if (!context.userId)
|
|
80
|
+
return false;
|
|
81
|
+
const params = strategy.parameters;
|
|
82
|
+
const userIds = params?.userIds ?? [];
|
|
83
|
+
if (userIds.length === 0)
|
|
84
|
+
return false;
|
|
85
|
+
return userIds.includes(context.userId);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* FLEXIBLE_ROLLOUT - percentage with custom stickiness
|
|
89
|
+
*/
|
|
90
|
+
evaluateFlexibleRollout(strategy, context) {
|
|
91
|
+
const params = strategy.parameters;
|
|
92
|
+
const rollout = params?.rollout ?? 0;
|
|
93
|
+
if (rollout <= 0)
|
|
94
|
+
return false;
|
|
95
|
+
if (rollout >= 100)
|
|
96
|
+
return true;
|
|
97
|
+
const stickiness = params?.stickiness ?? 'default';
|
|
98
|
+
const groupId = params?.groupId ?? strategy.name;
|
|
99
|
+
let identifier;
|
|
100
|
+
if (stickiness === 'default' || stickiness === 'userId') {
|
|
101
|
+
identifier = context.userId ?? context.sessionId;
|
|
102
|
+
}
|
|
103
|
+
else if (stickiness === 'sessionId') {
|
|
104
|
+
identifier = context.sessionId;
|
|
105
|
+
}
|
|
106
|
+
else if (stickiness === 'random') {
|
|
107
|
+
identifier = Math.random().toString();
|
|
108
|
+
}
|
|
109
|
+
else if (context.properties && stickiness in context.properties) {
|
|
110
|
+
const value = context.properties[stickiness];
|
|
111
|
+
identifier = value?.toString();
|
|
112
|
+
}
|
|
113
|
+
if (!identifier) {
|
|
114
|
+
identifier = Math.random().toString();
|
|
115
|
+
}
|
|
116
|
+
const hash = (0, murmurhash_js_1.normalizedHash)(groupId, identifier, 100);
|
|
117
|
+
return hash <= rollout;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* REMOTE_ADDRESS - check if IP matches allowed IPs/CIDRs
|
|
121
|
+
*/
|
|
122
|
+
evaluateRemoteAddress(strategy, context) {
|
|
123
|
+
if (!context.remoteAddress)
|
|
124
|
+
return false;
|
|
125
|
+
const params = strategy.parameters;
|
|
126
|
+
const ips = params?.ips ?? [];
|
|
127
|
+
if (ips.length === 0)
|
|
128
|
+
return false;
|
|
129
|
+
return (0, ip_matcher_js_1.matchIPList)(context.remoteAddress, ips);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Check if context satisfies all constraints
|
|
133
|
+
*/
|
|
134
|
+
checkConstraints(constraints, context) {
|
|
135
|
+
for (const constraint of constraints) {
|
|
136
|
+
if (!this.checkConstraint(constraint, context)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check a single constraint
|
|
144
|
+
*/
|
|
145
|
+
checkConstraint(constraint, context) {
|
|
146
|
+
const contextName = constraint['contextName'];
|
|
147
|
+
const operator = constraint['operator'];
|
|
148
|
+
const values = constraint['values'];
|
|
149
|
+
if (!contextName || !operator)
|
|
150
|
+
return true;
|
|
151
|
+
const contextValue = this.getContextValue(contextName, context);
|
|
152
|
+
switch (operator) {
|
|
153
|
+
case 'IN':
|
|
154
|
+
return values?.includes(contextValue) ?? false;
|
|
155
|
+
case 'NOT_IN':
|
|
156
|
+
return !(values?.includes(contextValue) ?? false);
|
|
157
|
+
case 'STR_ENDS_WITH':
|
|
158
|
+
if (typeof contextValue !== 'string' || !values)
|
|
159
|
+
return false;
|
|
160
|
+
return values.some((v) => typeof v === 'string' && contextValue.endsWith(v));
|
|
161
|
+
case 'STR_STARTS_WITH':
|
|
162
|
+
if (typeof contextValue !== 'string' || !values)
|
|
163
|
+
return false;
|
|
164
|
+
return values.some((v) => typeof v === 'string' && contextValue.startsWith(v));
|
|
165
|
+
case 'STR_CONTAINS':
|
|
166
|
+
if (typeof contextValue !== 'string' || !values)
|
|
167
|
+
return false;
|
|
168
|
+
return values.some((v) => typeof v === 'string' && contextValue.includes(v));
|
|
169
|
+
case 'NUM_EQ':
|
|
170
|
+
if (typeof contextValue !== 'number' || !values?.[0])
|
|
171
|
+
return false;
|
|
172
|
+
return contextValue === Number(values[0]);
|
|
173
|
+
case 'NUM_GT':
|
|
174
|
+
if (typeof contextValue !== 'number' || !values?.[0])
|
|
175
|
+
return false;
|
|
176
|
+
return contextValue > Number(values[0]);
|
|
177
|
+
case 'NUM_GTE':
|
|
178
|
+
if (typeof contextValue !== 'number' || !values?.[0])
|
|
179
|
+
return false;
|
|
180
|
+
return contextValue >= Number(values[0]);
|
|
181
|
+
case 'NUM_LT':
|
|
182
|
+
if (typeof contextValue !== 'number' || !values?.[0])
|
|
183
|
+
return false;
|
|
184
|
+
return contextValue < Number(values[0]);
|
|
185
|
+
case 'NUM_LTE':
|
|
186
|
+
if (typeof contextValue !== 'number' || !values?.[0])
|
|
187
|
+
return false;
|
|
188
|
+
return contextValue <= Number(values[0]);
|
|
189
|
+
default:
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get value from context by name
|
|
195
|
+
*/
|
|
196
|
+
getContextValue(name, context) {
|
|
197
|
+
switch (name) {
|
|
198
|
+
case 'userId':
|
|
199
|
+
return context.userId;
|
|
200
|
+
case 'sessionId':
|
|
201
|
+
return context.sessionId;
|
|
202
|
+
case 'remoteAddress':
|
|
203
|
+
return context.remoteAddress;
|
|
204
|
+
default:
|
|
205
|
+
return context.properties?.[name];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
exports.StrategyEvaluator = StrategyEvaluator;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SetGo SDK Error Classes
|
|
3
|
+
*/
|
|
4
|
+
export declare class SetGoError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
constructor(message: string, code: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class NetworkError extends SetGoError {
|
|
9
|
+
readonly statusCode?: number | undefined;
|
|
10
|
+
readonly cause?: Error | undefined;
|
|
11
|
+
constructor(message: string, statusCode?: number | undefined, cause?: Error | undefined);
|
|
12
|
+
}
|
|
13
|
+
export declare class ConfigurationError extends SetGoError {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class AuthenticationError extends SetGoError {
|
|
17
|
+
constructor(message?: string);
|
|
18
|
+
}
|
|
19
|
+
export declare class TimeoutError extends SetGoError {
|
|
20
|
+
constructor(message?: string);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,qBAAa,UAAW,SAAQ,KAAK;aAGjB,IAAI,EAAE,MAAM;gBAD5B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM;CAM/B;AAED,qBAAa,YAAa,SAAQ,UAAU;aAGxB,UAAU,CAAC,EAAE,MAAM;aACnB,KAAK,CAAC,EAAE,KAAK;gBAF7B,OAAO,EAAE,MAAM,EACC,UAAU,CAAC,EAAE,MAAM,YAAA,EACnB,KAAK,CAAC,EAAE,KAAK,YAAA;CAMhC;AAED,qBAAa,kBAAmB,SAAQ,UAAU;gBACpC,OAAO,EAAE,MAAM;CAK5B;AAED,qBAAa,mBAAoB,SAAQ,UAAU;gBACrC,OAAO,GAAE,MAAuC;CAK7D;AAED,qBAAa,YAAa,SAAQ,UAAU;gBAC9B,OAAO,GAAE,MAA4B;CAKlD"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SetGo SDK Error Classes
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TimeoutError = exports.AuthenticationError = exports.ConfigurationError = exports.NetworkError = exports.SetGoError = void 0;
|
|
7
|
+
class SetGoError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(message, code) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = 'SetGoError';
|
|
13
|
+
Object.setPrototypeOf(this, SetGoError.prototype);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.SetGoError = SetGoError;
|
|
17
|
+
class NetworkError extends SetGoError {
|
|
18
|
+
statusCode;
|
|
19
|
+
cause;
|
|
20
|
+
constructor(message, statusCode, cause) {
|
|
21
|
+
super(message, 'NETWORK_ERROR');
|
|
22
|
+
this.statusCode = statusCode;
|
|
23
|
+
this.cause = cause;
|
|
24
|
+
this.name = 'NetworkError';
|
|
25
|
+
Object.setPrototypeOf(this, NetworkError.prototype);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.NetworkError = NetworkError;
|
|
29
|
+
class ConfigurationError extends SetGoError {
|
|
30
|
+
constructor(message) {
|
|
31
|
+
super(message, 'CONFIGURATION_ERROR');
|
|
32
|
+
this.name = 'ConfigurationError';
|
|
33
|
+
Object.setPrototypeOf(this, ConfigurationError.prototype);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.ConfigurationError = ConfigurationError;
|
|
37
|
+
class AuthenticationError extends SetGoError {
|
|
38
|
+
constructor(message = 'Invalid or expired API token') {
|
|
39
|
+
super(message, 'AUTHENTICATION_ERROR');
|
|
40
|
+
this.name = 'AuthenticationError';
|
|
41
|
+
Object.setPrototypeOf(this, AuthenticationError.prototype);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.AuthenticationError = AuthenticationError;
|
|
45
|
+
class TimeoutError extends SetGoError {
|
|
46
|
+
constructor(message = 'Request timed out') {
|
|
47
|
+
super(message, 'TIMEOUT_ERROR');
|
|
48
|
+
this.name = 'TimeoutError';
|
|
49
|
+
Object.setPrototypeOf(this, TimeoutError.prototype);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.TimeoutError = TimeoutError;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SetGo SDK - Feature Flag Management
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { SetGoClient } from '@setgo/sdk';
|
|
7
|
+
*
|
|
8
|
+
* const client = new SetGoClient({
|
|
9
|
+
* baseUrl: process.env.SETGO_URL,
|
|
10
|
+
* token: process.env.SETGO_TOKEN,
|
|
11
|
+
* appName: 'my-app',
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* await client.start();
|
|
15
|
+
*
|
|
16
|
+
* if (client.isEnabled('dark-mode', { userId: '123' })) {
|
|
17
|
+
* // Feature is enabled
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export { SetGoClient } from './core/index.js';
|
|
22
|
+
export type { SetGoConfig, ResolvedSetGoConfig, FeatureContext, Feature, Strategy, StrategyType, FeaturesResponse, SetGoEventType, SetGoEventHandler, SetGoReadyEvent, SetGoUpdateEvent, SetGoErrorEvent, SetGoEvent, FeatureMetric, MetricsPayload, RegisterPayload, SetGoModuleAsyncOptions, } from './types/index.js';
|
|
23
|
+
export { SetGoError, NetworkError, ConfigurationError, AuthenticationError, TimeoutError, } from './errors/index.js';
|
|
24
|
+
export { FeatureRepository, StrategyEvaluator, MetricsReporter } from './core/index.js';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9C,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,aAAa,EACb,cAAc,EACd,eAAe,EACf,uBAAuB,GACxB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,UAAU,EACV,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SetGo SDK - Feature Flag Management
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* import { SetGoClient } from '@setgo/sdk';
|
|
8
|
+
*
|
|
9
|
+
* const client = new SetGoClient({
|
|
10
|
+
* baseUrl: process.env.SETGO_URL,
|
|
11
|
+
* token: process.env.SETGO_TOKEN,
|
|
12
|
+
* appName: 'my-app',
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* await client.start();
|
|
16
|
+
*
|
|
17
|
+
* if (client.isEnabled('dark-mode', { userId: '123' })) {
|
|
18
|
+
* // Feature is enabled
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.MetricsReporter = exports.StrategyEvaluator = exports.FeatureRepository = exports.TimeoutError = exports.AuthenticationError = exports.ConfigurationError = exports.NetworkError = exports.SetGoError = exports.SetGoClient = void 0;
|
|
24
|
+
// Main client
|
|
25
|
+
var index_js_1 = require("./core/index.js");
|
|
26
|
+
Object.defineProperty(exports, "SetGoClient", { enumerable: true, get: function () { return index_js_1.SetGoClient; } });
|
|
27
|
+
// Errors
|
|
28
|
+
var index_js_2 = require("./errors/index.js");
|
|
29
|
+
Object.defineProperty(exports, "SetGoError", { enumerable: true, get: function () { return index_js_2.SetGoError; } });
|
|
30
|
+
Object.defineProperty(exports, "NetworkError", { enumerable: true, get: function () { return index_js_2.NetworkError; } });
|
|
31
|
+
Object.defineProperty(exports, "ConfigurationError", { enumerable: true, get: function () { return index_js_2.ConfigurationError; } });
|
|
32
|
+
Object.defineProperty(exports, "AuthenticationError", { enumerable: true, get: function () { return index_js_2.AuthenticationError; } });
|
|
33
|
+
Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return index_js_2.TimeoutError; } });
|
|
34
|
+
// Internal (for advanced usage)
|
|
35
|
+
var index_js_3 = require("./core/index.js");
|
|
36
|
+
Object.defineProperty(exports, "FeatureRepository", { enumerable: true, get: function () { return index_js_3.FeatureRepository; } });
|
|
37
|
+
Object.defineProperty(exports, "StrategyEvaluator", { enumerable: true, get: function () { return index_js_3.StrategyEvaluator; } });
|
|
38
|
+
Object.defineProperty(exports, "MetricsReporter", { enumerable: true, get: function () { return index_js_3.MetricsReporter; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/nestjs/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SetGo NestJS Module Exports
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SETGO_CLIENT = exports.SETGO_CONFIG = exports.SetGoService = exports.SetGoModule = void 0;
|
|
7
|
+
var setgo_module_js_1 = require("./setgo.module.js");
|
|
8
|
+
Object.defineProperty(exports, "SetGoModule", { enumerable: true, get: function () { return setgo_module_js_1.SetGoModule; } });
|
|
9
|
+
var setgo_service_js_1 = require("./setgo.service.js");
|
|
10
|
+
Object.defineProperty(exports, "SetGoService", { enumerable: true, get: function () { return setgo_service_js_1.SetGoService; } });
|
|
11
|
+
var setgo_constants_js_1 = require("./setgo.constants.js");
|
|
12
|
+
Object.defineProperty(exports, "SETGO_CONFIG", { enumerable: true, get: function () { return setgo_constants_js_1.SETGO_CONFIG; } });
|
|
13
|
+
Object.defineProperty(exports, "SETGO_CLIENT", { enumerable: true, get: function () { return setgo_constants_js_1.SETGO_CLIENT; } });
|