@symbo.ls/sdk 2.34.32 → 2.34.33
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/dist/cjs/index.js +8 -0
- package/dist/cjs/services/FeatureFlagService.js +175 -0
- package/dist/cjs/services/index.js +4 -0
- package/dist/cjs/utils/services.js +8 -1
- package/dist/esm/index.js +171 -1
- package/dist/esm/services/FeatureFlagService.js +956 -0
- package/dist/esm/services/index.js +157 -0
- package/dist/esm/utils/services.js +8 -1
- package/dist/node/index.js +12 -2
- package/dist/node/services/FeatureFlagService.js +156 -0
- package/dist/node/services/index.js +4 -0
- package/dist/node/utils/services.js +8 -1
- package/package.json +6 -6
- package/src/index.js +11 -2
- package/src/services/FeatureFlagService.js +174 -0
- package/src/services/index.js +6 -1
- package/src/services/tests/FeatureFlagService/adminFeatureFlags.test.js +67 -0
- package/src/services/tests/FeatureFlagService/getFeatureFlags.test.js +75 -0
- package/src/utils/services.js +9 -1
|
@@ -50331,6 +50331,160 @@ var IntegrationService = class extends BaseService {
|
|
|
50331
50331
|
}
|
|
50332
50332
|
};
|
|
50333
50333
|
|
|
50334
|
+
// src/services/FeatureFlagService.js
|
|
50335
|
+
function normalizeKeysParam(keys2) {
|
|
50336
|
+
if (!keys2) {
|
|
50337
|
+
return null;
|
|
50338
|
+
}
|
|
50339
|
+
if (Array.isArray(keys2)) {
|
|
50340
|
+
const flattened = keys2.flatMap((k3) => String(k3).split(","));
|
|
50341
|
+
const cleaned2 = flattened.map((s3) => s3.trim()).filter(Boolean);
|
|
50342
|
+
return cleaned2.length ? cleaned2.join(",") : null;
|
|
50343
|
+
}
|
|
50344
|
+
const cleaned = String(keys2).split(",").map((s3) => s3.trim()).filter(Boolean);
|
|
50345
|
+
return cleaned.length ? cleaned.join(",") : null;
|
|
50346
|
+
}
|
|
50347
|
+
var FeatureFlagService = class extends BaseService {
|
|
50348
|
+
// ==================== USER FEATURE FLAGS (optional auth) ====================
|
|
50349
|
+
/**
|
|
50350
|
+
* Evaluate feature flags for the current user (or anonymous).
|
|
50351
|
+
* @param {Object} [params]
|
|
50352
|
+
* @param {string[]|string} [params.keys] Optional subset of keys (array or comma-separated string)
|
|
50353
|
+
* @returns {Promise<{flags: Record<string, {enabled: boolean, variant: string|null, payload: any}>}>}
|
|
50354
|
+
*/
|
|
50355
|
+
async getFeatureFlags(params2 = {}) {
|
|
50356
|
+
this._requireReady("getFeatureFlags");
|
|
50357
|
+
const { keys: keys2 } = params2 || {};
|
|
50358
|
+
const queryParams = new URLSearchParams();
|
|
50359
|
+
const keysParam = normalizeKeysParam(keys2);
|
|
50360
|
+
if (keysParam) {
|
|
50361
|
+
queryParams.append("keys", keysParam);
|
|
50362
|
+
}
|
|
50363
|
+
const queryString = queryParams.toString();
|
|
50364
|
+
const url2 = `/feature-flags${queryString ? `?${queryString}` : ""}`;
|
|
50365
|
+
try {
|
|
50366
|
+
const response = await this._request(url2, {
|
|
50367
|
+
method: "GET",
|
|
50368
|
+
methodName: "getFeatureFlags"
|
|
50369
|
+
});
|
|
50370
|
+
if (response.success) {
|
|
50371
|
+
return response.data;
|
|
50372
|
+
}
|
|
50373
|
+
throw new Error(response.message);
|
|
50374
|
+
} catch (error) {
|
|
50375
|
+
throw new Error(`Failed to get feature flags: ${error.message}`, { cause: error });
|
|
50376
|
+
}
|
|
50377
|
+
}
|
|
50378
|
+
/**
|
|
50379
|
+
* Evaluate a single feature flag for the current user (or anonymous).
|
|
50380
|
+
*/
|
|
50381
|
+
async getFeatureFlag(key) {
|
|
50382
|
+
this._requireReady("getFeatureFlag");
|
|
50383
|
+
if (!key) {
|
|
50384
|
+
throw new Error("Feature flag key is required");
|
|
50385
|
+
}
|
|
50386
|
+
try {
|
|
50387
|
+
const response = await this._request(`/feature-flags/${encodeURIComponent(String(key))}`, {
|
|
50388
|
+
method: "GET",
|
|
50389
|
+
methodName: "getFeatureFlag"
|
|
50390
|
+
});
|
|
50391
|
+
if (response.success) {
|
|
50392
|
+
return response.data;
|
|
50393
|
+
}
|
|
50394
|
+
throw new Error(response.message);
|
|
50395
|
+
} catch (error) {
|
|
50396
|
+
throw new Error(`Failed to get feature flag: ${error.message}`, { cause: error });
|
|
50397
|
+
}
|
|
50398
|
+
}
|
|
50399
|
+
// ==================== ADMIN FEATURE FLAGS (admin only) ====================
|
|
50400
|
+
async getAdminFeatureFlags(params2 = {}) {
|
|
50401
|
+
this._requireReady("getAdminFeatureFlags");
|
|
50402
|
+
const { includeArchived = true } = params2 || {};
|
|
50403
|
+
const queryParams = new URLSearchParams();
|
|
50404
|
+
if (includeArchived === false) {
|
|
50405
|
+
queryParams.append("includeArchived", "false");
|
|
50406
|
+
}
|
|
50407
|
+
const queryString = queryParams.toString();
|
|
50408
|
+
const url2 = `/admin/feature-flags${queryString ? `?${queryString}` : ""}`;
|
|
50409
|
+
try {
|
|
50410
|
+
const response = await this._request(url2, {
|
|
50411
|
+
method: "GET",
|
|
50412
|
+
methodName: "getAdminFeatureFlags"
|
|
50413
|
+
});
|
|
50414
|
+
if (response.success) {
|
|
50415
|
+
return response.data;
|
|
50416
|
+
}
|
|
50417
|
+
throw new Error(response.message);
|
|
50418
|
+
} catch (error) {
|
|
50419
|
+
throw new Error(`Failed to get admin feature flags: ${error.message}`, {
|
|
50420
|
+
cause: error
|
|
50421
|
+
});
|
|
50422
|
+
}
|
|
50423
|
+
}
|
|
50424
|
+
async createFeatureFlag(flagData) {
|
|
50425
|
+
this._requireReady("createFeatureFlag");
|
|
50426
|
+
if (!flagData || typeof flagData !== "object") {
|
|
50427
|
+
throw new Error("Feature flag data is required");
|
|
50428
|
+
}
|
|
50429
|
+
if (!flagData.key) {
|
|
50430
|
+
throw new Error("Feature flag key is required");
|
|
50431
|
+
}
|
|
50432
|
+
try {
|
|
50433
|
+
const response = await this._request("/admin/feature-flags", {
|
|
50434
|
+
method: "POST",
|
|
50435
|
+
body: JSON.stringify(flagData),
|
|
50436
|
+
methodName: "createFeatureFlag"
|
|
50437
|
+
});
|
|
50438
|
+
if (response.success) {
|
|
50439
|
+
return response.data;
|
|
50440
|
+
}
|
|
50441
|
+
throw new Error(response.message);
|
|
50442
|
+
} catch (error) {
|
|
50443
|
+
throw new Error(`Failed to create feature flag: ${error.message}`, { cause: error });
|
|
50444
|
+
}
|
|
50445
|
+
}
|
|
50446
|
+
async updateFeatureFlag(id2, patch) {
|
|
50447
|
+
this._requireReady("updateFeatureFlag");
|
|
50448
|
+
if (!id2) {
|
|
50449
|
+
throw new Error("Feature flag id is required");
|
|
50450
|
+
}
|
|
50451
|
+
if (!patch || typeof patch !== "object") {
|
|
50452
|
+
throw new Error("Feature flag patch is required");
|
|
50453
|
+
}
|
|
50454
|
+
try {
|
|
50455
|
+
const response = await this._request(`/admin/feature-flags/${encodeURIComponent(String(id2))}`, {
|
|
50456
|
+
method: "PATCH",
|
|
50457
|
+
body: JSON.stringify(patch),
|
|
50458
|
+
methodName: "updateFeatureFlag"
|
|
50459
|
+
});
|
|
50460
|
+
if (response.success) {
|
|
50461
|
+
return response.data;
|
|
50462
|
+
}
|
|
50463
|
+
throw new Error(response.message);
|
|
50464
|
+
} catch (error) {
|
|
50465
|
+
throw new Error(`Failed to update feature flag: ${error.message}`, { cause: error });
|
|
50466
|
+
}
|
|
50467
|
+
}
|
|
50468
|
+
async archiveFeatureFlag(id2) {
|
|
50469
|
+
this._requireReady("archiveFeatureFlag");
|
|
50470
|
+
if (!id2) {
|
|
50471
|
+
throw new Error("Feature flag id is required");
|
|
50472
|
+
}
|
|
50473
|
+
try {
|
|
50474
|
+
const response = await this._request(`/admin/feature-flags/${encodeURIComponent(String(id2))}`, {
|
|
50475
|
+
method: "DELETE",
|
|
50476
|
+
methodName: "archiveFeatureFlag"
|
|
50477
|
+
});
|
|
50478
|
+
if (response.success) {
|
|
50479
|
+
return response.data;
|
|
50480
|
+
}
|
|
50481
|
+
throw new Error(response.message);
|
|
50482
|
+
} catch (error) {
|
|
50483
|
+
throw new Error(`Failed to archive feature flag: ${error.message}`, { cause: error });
|
|
50484
|
+
}
|
|
50485
|
+
}
|
|
50486
|
+
};
|
|
50487
|
+
|
|
50334
50488
|
// src/services/index.js
|
|
50335
50489
|
var createService = (ServiceClass, config) => new ServiceClass(config);
|
|
50336
50490
|
var createAuthService = (config) => createService(AuthService, config);
|
|
@@ -50349,12 +50503,14 @@ var createTrackingService = (config) => createService(TrackingService, config);
|
|
|
50349
50503
|
var createWaitlistService = (config) => createService(WaitlistService, config);
|
|
50350
50504
|
var createMetricsService = (config) => createService(MetricsService, config);
|
|
50351
50505
|
var createIntegrationService = (config) => createService(IntegrationService, config);
|
|
50506
|
+
var createFeatureFlagService = (config) => createService(FeatureFlagService, config);
|
|
50352
50507
|
export {
|
|
50353
50508
|
AdminService,
|
|
50354
50509
|
AuthService,
|
|
50355
50510
|
BranchService,
|
|
50356
50511
|
CollabService,
|
|
50357
50512
|
DnsService,
|
|
50513
|
+
FeatureFlagService,
|
|
50358
50514
|
FileService,
|
|
50359
50515
|
IntegrationService,
|
|
50360
50516
|
MetricsService,
|
|
@@ -50371,6 +50527,7 @@ export {
|
|
|
50371
50527
|
createBranchService,
|
|
50372
50528
|
createCollabService,
|
|
50373
50529
|
createDnsService,
|
|
50530
|
+
createFeatureFlagService,
|
|
50374
50531
|
createFileService,
|
|
50375
50532
|
createIntegrationService,
|
|
50376
50533
|
createMetricsService,
|
|
@@ -298,7 +298,14 @@ var SERVICE_METHODS = {
|
|
|
298
298
|
listGitHubConnectors: "integration",
|
|
299
299
|
createGitHubConnector: "integration",
|
|
300
300
|
updateGitHubConnector: "integration",
|
|
301
|
-
deleteGitHubConnector: "integration"
|
|
301
|
+
deleteGitHubConnector: "integration",
|
|
302
|
+
// Feature flag methods (system-level + experiments)
|
|
303
|
+
getFeatureFlags: "featureFlag",
|
|
304
|
+
getFeatureFlag: "featureFlag",
|
|
305
|
+
getAdminFeatureFlags: "featureFlag",
|
|
306
|
+
createFeatureFlag: "featureFlag",
|
|
307
|
+
updateFeatureFlag: "featureFlag",
|
|
308
|
+
archiveFeatureFlag: "featureFlag"
|
|
302
309
|
};
|
|
303
310
|
export {
|
|
304
311
|
SERVICE_METHODS
|
package/dist/node/index.js
CHANGED
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
createTrackingService,
|
|
15
15
|
createWaitlistService,
|
|
16
16
|
createMetricsService,
|
|
17
|
-
createIntegrationService
|
|
17
|
+
createIntegrationService,
|
|
18
|
+
createFeatureFlagService
|
|
18
19
|
} from "./services/index.js";
|
|
19
20
|
import { SERVICE_METHODS } from "./utils/services.js";
|
|
20
21
|
import environment from "./config/environment.js";
|
|
@@ -155,6 +156,13 @@ class SDK {
|
|
|
155
156
|
context: this._context,
|
|
156
157
|
options: this._options
|
|
157
158
|
})
|
|
159
|
+
),
|
|
160
|
+
this._initService(
|
|
161
|
+
"featureFlag",
|
|
162
|
+
createFeatureFlagService({
|
|
163
|
+
context: this._context,
|
|
164
|
+
options: this._options
|
|
165
|
+
})
|
|
158
166
|
)
|
|
159
167
|
]);
|
|
160
168
|
return this;
|
|
@@ -286,7 +294,8 @@ import {
|
|
|
286
294
|
createTrackingService as createTrackingService2,
|
|
287
295
|
createWaitlistService as createWaitlistService2,
|
|
288
296
|
createMetricsService as createMetricsService2,
|
|
289
|
-
createIntegrationService as createIntegrationService2
|
|
297
|
+
createIntegrationService as createIntegrationService2,
|
|
298
|
+
createFeatureFlagService as createFeatureFlagService2
|
|
290
299
|
} from "./services/index.js";
|
|
291
300
|
import { default as default2 } from "./config/environment.js";
|
|
292
301
|
export {
|
|
@@ -296,6 +305,7 @@ export {
|
|
|
296
305
|
createBranchService2 as createBranchService,
|
|
297
306
|
createCollabService2 as createCollabService,
|
|
298
307
|
createDnsService2 as createDnsService,
|
|
308
|
+
createFeatureFlagService2 as createFeatureFlagService,
|
|
299
309
|
createFileService2 as createFileService,
|
|
300
310
|
createIntegrationService2 as createIntegrationService,
|
|
301
311
|
createMetricsService2 as createMetricsService,
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { BaseService } from "./BaseService.js";
|
|
2
|
+
function normalizeKeysParam(keys) {
|
|
3
|
+
if (!keys) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(keys)) {
|
|
7
|
+
const flattened = keys.flatMap((k) => String(k).split(","));
|
|
8
|
+
const cleaned2 = flattened.map((s) => s.trim()).filter(Boolean);
|
|
9
|
+
return cleaned2.length ? cleaned2.join(",") : null;
|
|
10
|
+
}
|
|
11
|
+
const cleaned = String(keys).split(",").map((s) => s.trim()).filter(Boolean);
|
|
12
|
+
return cleaned.length ? cleaned.join(",") : null;
|
|
13
|
+
}
|
|
14
|
+
class FeatureFlagService extends BaseService {
|
|
15
|
+
// ==================== USER FEATURE FLAGS (optional auth) ====================
|
|
16
|
+
/**
|
|
17
|
+
* Evaluate feature flags for the current user (or anonymous).
|
|
18
|
+
* @param {Object} [params]
|
|
19
|
+
* @param {string[]|string} [params.keys] Optional subset of keys (array or comma-separated string)
|
|
20
|
+
* @returns {Promise<{flags: Record<string, {enabled: boolean, variant: string|null, payload: any}>}>}
|
|
21
|
+
*/
|
|
22
|
+
async getFeatureFlags(params = {}) {
|
|
23
|
+
this._requireReady("getFeatureFlags");
|
|
24
|
+
const { keys } = params || {};
|
|
25
|
+
const queryParams = new URLSearchParams();
|
|
26
|
+
const keysParam = normalizeKeysParam(keys);
|
|
27
|
+
if (keysParam) {
|
|
28
|
+
queryParams.append("keys", keysParam);
|
|
29
|
+
}
|
|
30
|
+
const queryString = queryParams.toString();
|
|
31
|
+
const url = `/feature-flags${queryString ? `?${queryString}` : ""}`;
|
|
32
|
+
try {
|
|
33
|
+
const response = await this._request(url, {
|
|
34
|
+
method: "GET",
|
|
35
|
+
methodName: "getFeatureFlags"
|
|
36
|
+
});
|
|
37
|
+
if (response.success) {
|
|
38
|
+
return response.data;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(response.message);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`Failed to get feature flags: ${error.message}`, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Evaluate a single feature flag for the current user (or anonymous).
|
|
47
|
+
*/
|
|
48
|
+
async getFeatureFlag(key) {
|
|
49
|
+
this._requireReady("getFeatureFlag");
|
|
50
|
+
if (!key) {
|
|
51
|
+
throw new Error("Feature flag key is required");
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const response = await this._request(`/feature-flags/${encodeURIComponent(String(key))}`, {
|
|
55
|
+
method: "GET",
|
|
56
|
+
methodName: "getFeatureFlag"
|
|
57
|
+
});
|
|
58
|
+
if (response.success) {
|
|
59
|
+
return response.data;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(response.message);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`Failed to get feature flag: ${error.message}`, { cause: error });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ==================== ADMIN FEATURE FLAGS (admin only) ====================
|
|
67
|
+
async getAdminFeatureFlags(params = {}) {
|
|
68
|
+
this._requireReady("getAdminFeatureFlags");
|
|
69
|
+
const { includeArchived = true } = params || {};
|
|
70
|
+
const queryParams = new URLSearchParams();
|
|
71
|
+
if (includeArchived === false) {
|
|
72
|
+
queryParams.append("includeArchived", "false");
|
|
73
|
+
}
|
|
74
|
+
const queryString = queryParams.toString();
|
|
75
|
+
const url = `/admin/feature-flags${queryString ? `?${queryString}` : ""}`;
|
|
76
|
+
try {
|
|
77
|
+
const response = await this._request(url, {
|
|
78
|
+
method: "GET",
|
|
79
|
+
methodName: "getAdminFeatureFlags"
|
|
80
|
+
});
|
|
81
|
+
if (response.success) {
|
|
82
|
+
return response.data;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(response.message);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`Failed to get admin feature flags: ${error.message}`, {
|
|
87
|
+
cause: error
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async createFeatureFlag(flagData) {
|
|
92
|
+
this._requireReady("createFeatureFlag");
|
|
93
|
+
if (!flagData || typeof flagData !== "object") {
|
|
94
|
+
throw new Error("Feature flag data is required");
|
|
95
|
+
}
|
|
96
|
+
if (!flagData.key) {
|
|
97
|
+
throw new Error("Feature flag key is required");
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const response = await this._request("/admin/feature-flags", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
body: JSON.stringify(flagData),
|
|
103
|
+
methodName: "createFeatureFlag"
|
|
104
|
+
});
|
|
105
|
+
if (response.success) {
|
|
106
|
+
return response.data;
|
|
107
|
+
}
|
|
108
|
+
throw new Error(response.message);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw new Error(`Failed to create feature flag: ${error.message}`, { cause: error });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async updateFeatureFlag(id, patch) {
|
|
114
|
+
this._requireReady("updateFeatureFlag");
|
|
115
|
+
if (!id) {
|
|
116
|
+
throw new Error("Feature flag id is required");
|
|
117
|
+
}
|
|
118
|
+
if (!patch || typeof patch !== "object") {
|
|
119
|
+
throw new Error("Feature flag patch is required");
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const response = await this._request(`/admin/feature-flags/${encodeURIComponent(String(id))}`, {
|
|
123
|
+
method: "PATCH",
|
|
124
|
+
body: JSON.stringify(patch),
|
|
125
|
+
methodName: "updateFeatureFlag"
|
|
126
|
+
});
|
|
127
|
+
if (response.success) {
|
|
128
|
+
return response.data;
|
|
129
|
+
}
|
|
130
|
+
throw new Error(response.message);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new Error(`Failed to update feature flag: ${error.message}`, { cause: error });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async archiveFeatureFlag(id) {
|
|
136
|
+
this._requireReady("archiveFeatureFlag");
|
|
137
|
+
if (!id) {
|
|
138
|
+
throw new Error("Feature flag id is required");
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const response = await this._request(`/admin/feature-flags/${encodeURIComponent(String(id))}`, {
|
|
142
|
+
method: "DELETE",
|
|
143
|
+
methodName: "archiveFeatureFlag"
|
|
144
|
+
});
|
|
145
|
+
if (response.success) {
|
|
146
|
+
return response.data;
|
|
147
|
+
}
|
|
148
|
+
throw new Error(response.message);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new Error(`Failed to archive feature flag: ${error.message}`, { cause: error });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export {
|
|
155
|
+
FeatureFlagService
|
|
156
|
+
};
|
|
@@ -14,6 +14,7 @@ import { TrackingService } from "./TrackingService.js";
|
|
|
14
14
|
import { WaitlistService } from "./WaitlistService.js";
|
|
15
15
|
import { MetricsService } from "./MetricsService.js";
|
|
16
16
|
import { IntegrationService } from "./IntegrationService.js";
|
|
17
|
+
import { FeatureFlagService } from "./FeatureFlagService.js";
|
|
17
18
|
const createService = (ServiceClass, config) => new ServiceClass(config);
|
|
18
19
|
const createAuthService = (config) => createService(AuthService, config);
|
|
19
20
|
const createCollabService = (config) => createService(CollabService, config);
|
|
@@ -31,12 +32,14 @@ const createTrackingService = (config) => createService(TrackingService, config)
|
|
|
31
32
|
const createWaitlistService = (config) => createService(WaitlistService, config);
|
|
32
33
|
const createMetricsService = (config) => createService(MetricsService, config);
|
|
33
34
|
const createIntegrationService = (config) => createService(IntegrationService, config);
|
|
35
|
+
const createFeatureFlagService = (config) => createService(FeatureFlagService, config);
|
|
34
36
|
export {
|
|
35
37
|
AdminService,
|
|
36
38
|
AuthService,
|
|
37
39
|
BranchService,
|
|
38
40
|
CollabService,
|
|
39
41
|
DnsService,
|
|
42
|
+
FeatureFlagService,
|
|
40
43
|
FileService,
|
|
41
44
|
IntegrationService,
|
|
42
45
|
MetricsService,
|
|
@@ -53,6 +56,7 @@ export {
|
|
|
53
56
|
createBranchService,
|
|
54
57
|
createCollabService,
|
|
55
58
|
createDnsService,
|
|
59
|
+
createFeatureFlagService,
|
|
56
60
|
createFileService,
|
|
57
61
|
createIntegrationService,
|
|
58
62
|
createMetricsService,
|
|
@@ -297,7 +297,14 @@ const SERVICE_METHODS = {
|
|
|
297
297
|
listGitHubConnectors: "integration",
|
|
298
298
|
createGitHubConnector: "integration",
|
|
299
299
|
updateGitHubConnector: "integration",
|
|
300
|
-
deleteGitHubConnector: "integration"
|
|
300
|
+
deleteGitHubConnector: "integration",
|
|
301
|
+
// Feature flag methods (system-level + experiments)
|
|
302
|
+
getFeatureFlags: "featureFlag",
|
|
303
|
+
getFeatureFlag: "featureFlag",
|
|
304
|
+
getAdminFeatureFlags: "featureFlag",
|
|
305
|
+
createFeatureFlag: "featureFlag",
|
|
306
|
+
updateFeatureFlag: "featureFlag",
|
|
307
|
+
archiveFeatureFlag: "featureFlag"
|
|
301
308
|
};
|
|
302
309
|
export {
|
|
303
310
|
SERVICE_METHODS
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/sdk",
|
|
3
|
-
"version": "2.34.
|
|
3
|
+
"version": "2.34.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"test:unit-all": "cross-env NODE_ENV=$NODE_ENV npx tape src/services/tests/**/*.test.js | tap-spec"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@domql/element": "^2.34.
|
|
51
|
-
"@domql/utils": "^2.34.
|
|
50
|
+
"@domql/element": "^2.34.33",
|
|
51
|
+
"@domql/utils": "^2.34.33",
|
|
52
52
|
"@grafana/faro-web-sdk": "^1.19.0",
|
|
53
53
|
"@grafana/faro-web-tracing": "^1.19.0",
|
|
54
|
-
"@symbo.ls/router": "^2.34.
|
|
55
|
-
"@symbo.ls/socket": "^2.34.
|
|
54
|
+
"@symbo.ls/router": "^2.34.33",
|
|
55
|
+
"@symbo.ls/socket": "^2.34.33",
|
|
56
56
|
"acorn": "^8.14.0",
|
|
57
57
|
"acorn-walk": "^8.3.4",
|
|
58
58
|
"dexie": "^4.0.11",
|
|
@@ -75,5 +75,5 @@
|
|
|
75
75
|
"tap-spec": "^5.0.0",
|
|
76
76
|
"tape": "^5.9.0"
|
|
77
77
|
},
|
|
78
|
-
"gitHead": "
|
|
78
|
+
"gitHead": "a7ecaaa2792aea530bf0b4cef625904c63a0272a"
|
|
79
79
|
}
|
package/src/index.js
CHANGED
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
createTrackingService,
|
|
15
15
|
createWaitlistService,
|
|
16
16
|
createMetricsService,
|
|
17
|
-
createIntegrationService
|
|
17
|
+
createIntegrationService,
|
|
18
|
+
createFeatureFlagService
|
|
18
19
|
} from './services/index.js'
|
|
19
20
|
|
|
20
21
|
import { SERVICE_METHODS } from './utils/services.js'
|
|
@@ -169,6 +170,13 @@ export class SDK {
|
|
|
169
170
|
context: this._context,
|
|
170
171
|
options: this._options
|
|
171
172
|
})
|
|
173
|
+
),
|
|
174
|
+
this._initService(
|
|
175
|
+
'featureFlag',
|
|
176
|
+
createFeatureFlagService({
|
|
177
|
+
context: this._context,
|
|
178
|
+
options: this._options
|
|
179
|
+
})
|
|
172
180
|
)
|
|
173
181
|
])
|
|
174
182
|
|
|
@@ -335,7 +343,8 @@ export {
|
|
|
335
343
|
createTrackingService,
|
|
336
344
|
createWaitlistService,
|
|
337
345
|
createMetricsService,
|
|
338
|
-
createIntegrationService
|
|
346
|
+
createIntegrationService,
|
|
347
|
+
createFeatureFlagService
|
|
339
348
|
} from './services/index.js'
|
|
340
349
|
|
|
341
350
|
// Export environment configuration
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { BaseService } from './BaseService.js'
|
|
2
|
+
|
|
3
|
+
function normalizeKeysParam (keys) {
|
|
4
|
+
if (!keys) { return null }
|
|
5
|
+
if (Array.isArray(keys)) {
|
|
6
|
+
const flattened = keys.flatMap(k => String(k).split(','))
|
|
7
|
+
const cleaned = flattened.map(s => s.trim()).filter(Boolean)
|
|
8
|
+
return cleaned.length ? cleaned.join(',') : null
|
|
9
|
+
}
|
|
10
|
+
const cleaned = String(keys)
|
|
11
|
+
.split(',')
|
|
12
|
+
.map(s => s.trim())
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
return cleaned.length ? cleaned.join(',') : null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class FeatureFlagService extends BaseService {
|
|
18
|
+
// ==================== USER FEATURE FLAGS (optional auth) ====================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate feature flags for the current user (or anonymous).
|
|
22
|
+
* @param {Object} [params]
|
|
23
|
+
* @param {string[]|string} [params.keys] Optional subset of keys (array or comma-separated string)
|
|
24
|
+
* @returns {Promise<{flags: Record<string, {enabled: boolean, variant: string|null, payload: any}>}>}
|
|
25
|
+
*/
|
|
26
|
+
async getFeatureFlags (params = {}) {
|
|
27
|
+
this._requireReady('getFeatureFlags')
|
|
28
|
+
const { keys } = params || {}
|
|
29
|
+
|
|
30
|
+
const queryParams = new URLSearchParams()
|
|
31
|
+
const keysParam = normalizeKeysParam(keys)
|
|
32
|
+
if (keysParam) {
|
|
33
|
+
queryParams.append('keys', keysParam)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const queryString = queryParams.toString()
|
|
37
|
+
const url = `/feature-flags${queryString ? `?${queryString}` : ''}`
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await this._request(url, {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
methodName: 'getFeatureFlags'
|
|
43
|
+
})
|
|
44
|
+
if (response.success) {
|
|
45
|
+
return response.data
|
|
46
|
+
}
|
|
47
|
+
throw new Error(response.message)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`Failed to get feature flags: ${error.message}`, { cause: error })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Evaluate a single feature flag for the current user (or anonymous).
|
|
55
|
+
*/
|
|
56
|
+
async getFeatureFlag (key) {
|
|
57
|
+
this._requireReady('getFeatureFlag')
|
|
58
|
+
if (!key) {
|
|
59
|
+
throw new Error('Feature flag key is required')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await this._request(`/feature-flags/${encodeURIComponent(String(key))}`, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
methodName: 'getFeatureFlag'
|
|
66
|
+
})
|
|
67
|
+
if (response.success) {
|
|
68
|
+
return response.data
|
|
69
|
+
}
|
|
70
|
+
throw new Error(response.message)
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(`Failed to get feature flag: ${error.message}`, { cause: error })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ==================== ADMIN FEATURE FLAGS (admin only) ====================
|
|
77
|
+
|
|
78
|
+
async getAdminFeatureFlags (params = {}) {
|
|
79
|
+
this._requireReady('getAdminFeatureFlags')
|
|
80
|
+
const { includeArchived = true } = params || {}
|
|
81
|
+
|
|
82
|
+
const queryParams = new URLSearchParams()
|
|
83
|
+
if (includeArchived === false) {
|
|
84
|
+
queryParams.append('includeArchived', 'false')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const queryString = queryParams.toString()
|
|
88
|
+
const url = `/admin/feature-flags${queryString ? `?${queryString}` : ''}`
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const response = await this._request(url, {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
methodName: 'getAdminFeatureFlags'
|
|
94
|
+
})
|
|
95
|
+
if (response.success) {
|
|
96
|
+
return response.data
|
|
97
|
+
}
|
|
98
|
+
throw new Error(response.message)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new Error(`Failed to get admin feature flags: ${error.message}`, {
|
|
101
|
+
cause: error
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async createFeatureFlag (flagData) {
|
|
107
|
+
this._requireReady('createFeatureFlag')
|
|
108
|
+
if (!flagData || typeof flagData !== 'object') {
|
|
109
|
+
throw new Error('Feature flag data is required')
|
|
110
|
+
}
|
|
111
|
+
if (!flagData.key) {
|
|
112
|
+
throw new Error('Feature flag key is required')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const response = await this._request('/admin/feature-flags', {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
body: JSON.stringify(flagData),
|
|
119
|
+
methodName: 'createFeatureFlag'
|
|
120
|
+
})
|
|
121
|
+
if (response.success) {
|
|
122
|
+
return response.data
|
|
123
|
+
}
|
|
124
|
+
throw new Error(response.message)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(`Failed to create feature flag: ${error.message}`, { cause: error })
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async updateFeatureFlag (id, patch) {
|
|
131
|
+
this._requireReady('updateFeatureFlag')
|
|
132
|
+
if (!id) {
|
|
133
|
+
throw new Error('Feature flag id is required')
|
|
134
|
+
}
|
|
135
|
+
if (!patch || typeof patch !== 'object') {
|
|
136
|
+
throw new Error('Feature flag patch is required')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const response = await this._request(`/admin/feature-flags/${encodeURIComponent(String(id))}`, {
|
|
141
|
+
method: 'PATCH',
|
|
142
|
+
body: JSON.stringify(patch),
|
|
143
|
+
methodName: 'updateFeatureFlag'
|
|
144
|
+
})
|
|
145
|
+
if (response.success) {
|
|
146
|
+
return response.data
|
|
147
|
+
}
|
|
148
|
+
throw new Error(response.message)
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new Error(`Failed to update feature flag: ${error.message}`, { cause: error })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async archiveFeatureFlag (id) {
|
|
155
|
+
this._requireReady('archiveFeatureFlag')
|
|
156
|
+
if (!id) {
|
|
157
|
+
throw new Error('Feature flag id is required')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const response = await this._request(`/admin/feature-flags/${encodeURIComponent(String(id))}`, {
|
|
162
|
+
method: 'DELETE',
|
|
163
|
+
methodName: 'archiveFeatureFlag'
|
|
164
|
+
})
|
|
165
|
+
if (response.success) {
|
|
166
|
+
return response.data
|
|
167
|
+
}
|
|
168
|
+
throw new Error(response.message)
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new Error(`Failed to archive feature flag: ${error.message}`, { cause: error })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|