@forge/feature-flags 0.0.0-experimental-e2ec7ba
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 +11 -0
- package/LICENSE.txt +7 -0
- package/README.md +204 -0
- package/out/evaluator.d.ts +13 -0
- package/out/evaluator.d.ts.map +1 -0
- package/out/evaluator.js +97 -0
- package/out/featureFlags.d.ts +24 -0
- package/out/featureFlags.d.ts.map +1 -0
- package/out/featureFlags.js +135 -0
- package/out/index.d.ts +3 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +7 -0
- package/out/types/index.d.ts +94 -0
- package/out/types/index.d.ts.map +1 -0
- package/out/types/index.js +14 -0
- package/out/utils/conditions.d.ts +3 -0
- package/out/utils/conditions.d.ts.map +1 -0
- package/out/utils/conditions.js +78 -0
- package/out/utils/hash.d.ts +2 -0
- package/out/utils/hash.d.ts.map +1 -0
- package/out/utils/hash.js +10 -0
- package/package.json +23 -0
package/CHANGELOG.md
ADDED
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 Atlassian
|
|
2
|
+
Permission is hereby granted to use this software in accordance with the terms
|
|
3
|
+
and conditions outlined in the Atlassian Developer Terms, which can be found
|
|
4
|
+
at the following URL:
|
|
5
|
+
https://developer.atlassian.com/platform/marketplace/atlassian-developer-terms/
|
|
6
|
+
By using this software, you agree to comply with these terms and conditions.
|
|
7
|
+
If you do not agree with these terms, you are not permitted to use this software.
|
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# @forge/feature-flags
|
|
2
|
+
|
|
3
|
+
A feature flag SDK for Atlassian Forge apps running on Forge Node Runtime. This package provides a simple interface for evaluating feature flags.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @forge/feature-flags
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { getAppContext } from "@forge/api";
|
|
17
|
+
import { FeatureFlags } from "@forge/feature-flags";
|
|
18
|
+
|
|
19
|
+
export const handler = async (payload, context) => {
|
|
20
|
+
// Get app context values
|
|
21
|
+
const {
|
|
22
|
+
appVersion,
|
|
23
|
+
license: appLicense,
|
|
24
|
+
environmentType,
|
|
25
|
+
} = getAppContext();
|
|
26
|
+
|
|
27
|
+
// Determine license value based on trialEndDate and isActive
|
|
28
|
+
let licenseValue = "INACTIVE";
|
|
29
|
+
const trialEndDate = appLicense?.trialEndDate;
|
|
30
|
+
const isActive = appLicense?.isActive;
|
|
31
|
+
|
|
32
|
+
if (trialEndDate) {
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const trialEnd = new Date(trialEndDate);
|
|
35
|
+
if (trialEnd > now) {
|
|
36
|
+
licenseValue = "TRIAL";
|
|
37
|
+
} else if (isActive) {
|
|
38
|
+
licenseValue = "ACTIVE";
|
|
39
|
+
}
|
|
40
|
+
} else if (isActive) {
|
|
41
|
+
licenseValue = "ACTIVE";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Determine capabilitySet value (enum)
|
|
45
|
+
let capabilitySetValue = "capabilityStandard";
|
|
46
|
+
if (appLicense?.capabilitySet === "capabilityAdvanced") {
|
|
47
|
+
capabilitySetValue = "capabilityAdvanced";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initialize the feature flags SDK
|
|
51
|
+
const featureFlags = new FeatureFlags();
|
|
52
|
+
await featureFlags.initialize({
|
|
53
|
+
environment: environmentType?.toLowerCase() || "development"
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Define a user with all possible attributes that will be used in the feature flag rules
|
|
57
|
+
const user = {
|
|
58
|
+
identifiers: {
|
|
59
|
+
accountId: context?.principal?.accountId,
|
|
60
|
+
},
|
|
61
|
+
attributes: {
|
|
62
|
+
installContext: context?.installContext,
|
|
63
|
+
accountId: context?.principal?.accountId,
|
|
64
|
+
appVersion: appVersion,
|
|
65
|
+
license: licenseValue, // "ACTIVE", "INACTIVE", "TRIAL"
|
|
66
|
+
capabilitySet: capabilitySetValue // "capabilityAdvanced", "capabilityStandard"
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Check a feature flag (synchronous after initialization)
|
|
71
|
+
const isEnabled = featureFlags.checkFlag(user, "new-feature");
|
|
72
|
+
|
|
73
|
+
// Check with a default value if flag doesn't exist
|
|
74
|
+
const isEnabledWithDefault = featureFlags.checkFlag(user, "another-feature", true);
|
|
75
|
+
|
|
76
|
+
// Get all available flag IDs
|
|
77
|
+
const allFlagIds = featureFlags.getAllFlagIds();
|
|
78
|
+
|
|
79
|
+
// Shutdown when done (cleans up polling and resources)
|
|
80
|
+
featureFlags.shutdown();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## ⚠️ Important: Initialization Best Practices
|
|
86
|
+
|
|
87
|
+
**ALWAYS initialize the FeatureFlags SDK inside your handler function, NEVER initialize globally as it will use the stale feature flags**
|
|
88
|
+
|
|
89
|
+
### ✅ Correct Pattern - Initialize Inside Handler
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { getAppContext } from "@forge/api";
|
|
93
|
+
import { FeatureFlags } from "@forge/feature-flags";
|
|
94
|
+
|
|
95
|
+
export const handler = async (payload, context) => {
|
|
96
|
+
// ✅ Initialize inside the handler function
|
|
97
|
+
const featureFlags = new FeatureFlags();
|
|
98
|
+
await featureFlags.initialize({
|
|
99
|
+
environment: getAppContext().environmentType?.toLowerCase() || "development"
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Define a user
|
|
103
|
+
const user = {
|
|
104
|
+
identifiers: {
|
|
105
|
+
installContext: context?.installContext,
|
|
106
|
+
},
|
|
107
|
+
attributes: {
|
|
108
|
+
issues: 4
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Use feature flags...
|
|
113
|
+
const isEnabled = featureFlags.checkFlag(user, "new-feature");
|
|
114
|
+
|
|
115
|
+
// Shutdown when done (synchronous)
|
|
116
|
+
featureFlags.shutdown();
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### ❌ Incorrect Pattern - Global Initialization
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// ❌ NEVER do this - Global initialization
|
|
124
|
+
const featureFlags = new FeatureFlags();
|
|
125
|
+
await featureFlags.initialize(); // This will cause problems!
|
|
126
|
+
|
|
127
|
+
export const handler = async (payload, context) => {
|
|
128
|
+
// This will fail if token expires or network issues occur
|
|
129
|
+
const isEnabled = featureFlags.checkFlag(user, "new-feature");
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Polling Behavior
|
|
134
|
+
|
|
135
|
+
The package automatically polls for feature flag updates every 60 seconds:
|
|
136
|
+
|
|
137
|
+
1. **Cache Updates**: New data is cached for fallback purposes
|
|
138
|
+
2. **Error Resilience**: If a fetch fails, the package falls back to cached data
|
|
139
|
+
3. **Immediate Updates**: Feature flag changes are reflected within 60 seconds
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Polling happens automatically in the background
|
|
143
|
+
const featureFlags = new FeatureFlags();
|
|
144
|
+
await featureFlags.initialize();
|
|
145
|
+
|
|
146
|
+
// Feature flags are automatically updated every 60 seconds
|
|
147
|
+
// No manual intervention required
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## API Reference
|
|
151
|
+
|
|
152
|
+
### `FeatureFlags`
|
|
153
|
+
|
|
154
|
+
#### `constructor()`
|
|
155
|
+
Creates a new instance of the FeatureFlags class.
|
|
156
|
+
|
|
157
|
+
#### `initialize(config?: ForgeFeatureFlagConfig): Promise<void>`
|
|
158
|
+
Initializes the feature flags service with the provided configuration.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface ForgeFeatureFlagConfig {
|
|
162
|
+
environment?: 'development' | 'staging' | 'production';
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `checkFlag(user: User, flagId: string, defaultValue?: boolean): boolean`
|
|
167
|
+
Checks if a feature flag is enabled for the given user. Returns the `defaultValue` (defaults to `false`) if the flag doesn't exist. **Synchronous** after initialization.
|
|
168
|
+
|
|
169
|
+
#### `getFlag(flagId: string): FeatureFlag | undefined`
|
|
170
|
+
Gets a specific flag configuration. Useful for debugging purposes.
|
|
171
|
+
|
|
172
|
+
#### `getAllFlagIds(): string[]`
|
|
173
|
+
Returns an array of all available flag IDs.
|
|
174
|
+
|
|
175
|
+
#### `refresh(): Promise<void>`
|
|
176
|
+
Forces a refresh of feature flag configurations from the server.
|
|
177
|
+
|
|
178
|
+
#### `getLastFetchTime(): number`
|
|
179
|
+
Returns the timestamp of the last successful fetch from the server.
|
|
180
|
+
|
|
181
|
+
#### `isInitialized(): boolean`
|
|
182
|
+
Checks if the service is initialized.
|
|
183
|
+
|
|
184
|
+
#### `shutdown(): void`
|
|
185
|
+
Shuts down the feature flags service, stops polling, and cleans up resources. **Synchronous**.
|
|
186
|
+
|
|
187
|
+
### User Interface
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
interface User {
|
|
191
|
+
attributes?: Record<string, string | number>;
|
|
192
|
+
identifiers?: {
|
|
193
|
+
installContext?: string;
|
|
194
|
+
accountId?: string;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Configuration Options
|
|
200
|
+
|
|
201
|
+
| Option | Type | Default | Description |
|
|
202
|
+
|--------|------|---------|-------------|
|
|
203
|
+
| `environment` | `'development' \| 'staging' \| 'production'` | `'development'` | Environment tier for feature flag evaluation |
|
|
204
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FeatureFlag, User, EvaluationResult, Environment } from './types';
|
|
2
|
+
export declare class Evaluator {
|
|
3
|
+
private readonly environment;
|
|
4
|
+
constructor(options: {
|
|
5
|
+
environment: Environment;
|
|
6
|
+
});
|
|
7
|
+
evaluate(user: User, flag: FeatureFlag): EvaluationResult;
|
|
8
|
+
private evaluateOverrides;
|
|
9
|
+
private evaluateRule;
|
|
10
|
+
private evaluatePassPercentage;
|
|
11
|
+
private getUserId;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../src/evaluator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,IAAI,EAAkB,gBAAgB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAc3F,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;gBAC9B,OAAO,EAAE;QAAE,WAAW,EAAE,WAAW,CAAA;KAAE;IAM1C,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,GAAG,gBAAgB;IA4ChE,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,YAAY;IAwBpB,OAAO,CAAC,sBAAsB;IAqC9B,OAAO,CAAC,SAAS;CAGlB"}
|
package/out/evaluator.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Evaluator = void 0;
|
|
4
|
+
const hash_1 = require("./utils/hash");
|
|
5
|
+
const conditions_1 = require("./utils/conditions");
|
|
6
|
+
const api_1 = require("@forge/api");
|
|
7
|
+
const CONDITION_SEGMENT_COUNT = 10000;
|
|
8
|
+
class Evaluator {
|
|
9
|
+
environment;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.environment = options.environment;
|
|
12
|
+
}
|
|
13
|
+
evaluate(user, flag) {
|
|
14
|
+
if (!flag.isEnabled) {
|
|
15
|
+
return {
|
|
16
|
+
value: false,
|
|
17
|
+
ruleId: null,
|
|
18
|
+
reason: 'disabled'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const overrideResult = this.evaluateOverrides(user, flag?.overrides);
|
|
22
|
+
if (overrideResult) {
|
|
23
|
+
return overrideResult;
|
|
24
|
+
}
|
|
25
|
+
const rulesForEnv = [...flag.rules]
|
|
26
|
+
.filter((rule) => rule.env.includes(this.environment))
|
|
27
|
+
.sort((a, b) => a.order - b.order);
|
|
28
|
+
for (const rule of rulesForEnv) {
|
|
29
|
+
const ruleResult = this.evaluateRule(user, rule, flag);
|
|
30
|
+
if (ruleResult !== null) {
|
|
31
|
+
return {
|
|
32
|
+
value: ruleResult,
|
|
33
|
+
ruleId: rule.name,
|
|
34
|
+
reason: 'rule_match'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
value: false,
|
|
40
|
+
ruleId: null,
|
|
41
|
+
reason: 'default'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
evaluateOverrides(user, overrides) {
|
|
45
|
+
if (!overrides) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const overridesForEnv = [...overrides]
|
|
49
|
+
.filter((override) => override.env.includes(this.environment))
|
|
50
|
+
.sort((a, b) => a.order - b.order);
|
|
51
|
+
for (const override of overridesForEnv) {
|
|
52
|
+
const userValue = user?.attributes?.[override.attribute];
|
|
53
|
+
if (userValue !== undefined && override.values.includes(String(userValue))) {
|
|
54
|
+
return {
|
|
55
|
+
value: override.returnValue.value,
|
|
56
|
+
ruleId: `override:${override.attribute}`,
|
|
57
|
+
reason: 'override'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
evaluateRule(user, rule, flag) {
|
|
64
|
+
const conditionResults = rule.conditions.map((condition) => (0, conditions_1.evaluateCondition)(user, condition));
|
|
65
|
+
const conditionsPassed = conditionResults.every((result) => result === true);
|
|
66
|
+
if (!conditionsPassed) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const passesPercentageCheck = this.evaluatePassPercentage(user, rule, flag);
|
|
70
|
+
return passesPercentageCheck ? rule.returnValue.value : false;
|
|
71
|
+
}
|
|
72
|
+
evaluatePassPercentage(user, rule, flag) {
|
|
73
|
+
if (rule.passPercentage === undefined) {
|
|
74
|
+
console.warn(`passPercentage is undefined for rule '${rule.name}' in flag '${flag.flagId}'. Defaulting to false.`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (rule.passPercentage === 0) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (rule.passPercentage === 100) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
const userId = this.getUserId(user, flag.idType);
|
|
84
|
+
if (!userId) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const appId = (0, api_1.__getRuntime)()?.appContext?.appId || 'unknown';
|
|
88
|
+
const hashInput = `${appId}.${flag.flagId}.${rule.name}.${userId}`;
|
|
89
|
+
const hash = (0, hash_1.computeHash)(hashInput);
|
|
90
|
+
const bucket = Number(hash % BigInt(CONDITION_SEGMENT_COUNT));
|
|
91
|
+
return bucket < rule.passPercentage * 100;
|
|
92
|
+
}
|
|
93
|
+
getUserId(user, idType) {
|
|
94
|
+
return user?.identifiers?.[idType];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.Evaluator = Evaluator;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FeatureFlag, User, ForgeFeatureFlagConfig } from './types';
|
|
2
|
+
export declare class FeatureFlags {
|
|
3
|
+
private flags;
|
|
4
|
+
private evaluator;
|
|
5
|
+
private initialized;
|
|
6
|
+
private readonly pollingIntervalMs?;
|
|
7
|
+
private pollingTimer?;
|
|
8
|
+
private lastFetchTime;
|
|
9
|
+
private readonly metrics;
|
|
10
|
+
private readonly tags;
|
|
11
|
+
initialize(config?: ForgeFeatureFlagConfig): Promise<void>;
|
|
12
|
+
private fetchConfigurations;
|
|
13
|
+
checkFlag(user: User, flagId: string, defaultValue?: boolean): boolean;
|
|
14
|
+
getFlag(flagId: string): FeatureFlag | undefined;
|
|
15
|
+
getAllFlagIds(): string[];
|
|
16
|
+
refresh(): Promise<void>;
|
|
17
|
+
private startPolling;
|
|
18
|
+
private stopPolling;
|
|
19
|
+
private ensureInitialized;
|
|
20
|
+
isInitialized(): boolean;
|
|
21
|
+
getLastFetchTime(): number;
|
|
22
|
+
shutdown(): void;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=featureFlags.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"featureFlags.d.ts","sourceRoot":"","sources":["../src/featureFlags.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,sBAAsB,EAAW,MAAM,SAAS,CAAC;AAM7E,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAyB;IAC5D,OAAO,CAAC,YAAY,CAAC,CAAiB;IACtC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;IACnE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAGnB;IAKI,UAAU,CAAC,MAAM,GAAE,sBAAuC,GAAG,OAAO,CAAC,IAAI,CAAC;YAyBlE,mBAAmB;IAoDjC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,UAAQ,GAAG,OAAO;IAgBpE,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAQhD,aAAa,IAAI,MAAM,EAAE;IAQnB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,iBAAiB;IASzB,aAAa,IAAI,OAAO;IAOxB,gBAAgB,IAAI,MAAM;IAQ1B,QAAQ,IAAI,IAAI;CAMjB"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeatureFlags = void 0;
|
|
4
|
+
const api_1 = require("@forge/api");
|
|
5
|
+
const evaluator_1 = require("./evaluator");
|
|
6
|
+
const DEFAULT_CONFIG = { environment: 'development' };
|
|
7
|
+
class FeatureFlags {
|
|
8
|
+
flags = new Map();
|
|
9
|
+
evaluator;
|
|
10
|
+
initialized = false;
|
|
11
|
+
pollingIntervalMs = 1 * 60 * 1000;
|
|
12
|
+
pollingTimer;
|
|
13
|
+
lastFetchTime = 0;
|
|
14
|
+
metrics = (0, api_1.__getRuntime)()?.metrics;
|
|
15
|
+
tags = {
|
|
16
|
+
forgeEnv: (0, api_1.__getRuntime)()?.appContext?.environmentType || 'unknown',
|
|
17
|
+
appId: (0, api_1.__getRuntime)()?.appContext?.appId || 'unknown'
|
|
18
|
+
};
|
|
19
|
+
async initialize(config = DEFAULT_CONFIG) {
|
|
20
|
+
this.metrics?.counter('forge.feature-flags.initialize', this.tags).incr();
|
|
21
|
+
const timer = this.metrics?.timing('forge.feature-flags.initialize', this.tags).measure();
|
|
22
|
+
try {
|
|
23
|
+
await this.fetchConfigurations();
|
|
24
|
+
this.initialized = true;
|
|
25
|
+
this.metrics?.counter('forge.feature-flags.initialize.success', this.tags).incr();
|
|
26
|
+
if (this.pollingIntervalMs && (0, api_1.__getRuntime)()?.lambdaContext?.getRemainingTimeInMillis?.() > 60000) {
|
|
27
|
+
this.startPolling();
|
|
28
|
+
}
|
|
29
|
+
this.evaluator = new evaluator_1.Evaluator({ environment: config.environment });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
this.initialized = false;
|
|
33
|
+
this.metrics?.counter('forge.feature-flags.initialize.failure', this.tags).incr();
|
|
34
|
+
console.error('Failed to initialize Forge Feature Flags SDK');
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
timer?.stop();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async fetchConfigurations() {
|
|
41
|
+
try {
|
|
42
|
+
const runtime = (0, api_1.__getRuntime)();
|
|
43
|
+
if (runtime?.proxy?.tokenExpiry && runtime?.proxy?.tokenExpiry * 1000 < Date.now()) {
|
|
44
|
+
this.metrics?.counter('forge.feature-flags.fetchFromAtlassianServers.tokenExpired', this.tags).incr();
|
|
45
|
+
throw new Error('Cannot fetch feature flags: authentication token has expired');
|
|
46
|
+
}
|
|
47
|
+
const remainingTime = runtime?.lambdaContext?.getRemainingTimeInMillis?.();
|
|
48
|
+
if (remainingTime !== undefined && remainingTime < 5000) {
|
|
49
|
+
this.metrics?.counter('forge.feature-flags.fetchFromAtlassianServers.remainingTimeComplete', this.tags).incr();
|
|
50
|
+
throw new Error('Cannot fetch feature flags: insufficient remaining execution time');
|
|
51
|
+
}
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
54
|
+
const response = await (0, api_1.__fetchProduct)({ provider: 'app', remote: 'feature-flags', type: 'feature-flags' })('/', {
|
|
55
|
+
method: 'GET',
|
|
56
|
+
redirect: 'follow',
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
'x-b3-traceid': runtime?.tracing?.traceId,
|
|
61
|
+
'x-b3-spanid': runtime?.tracing?.spanId
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
clearTimeout(timeoutId);
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
this.flags = new Map(data?.feature_flags?.map((flag) => [flag.flagId, flag]));
|
|
70
|
+
this.lastFetchTime = Date.now();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.error('Error fetching feature flag configurations:', error);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
checkFlag(user, flagId, defaultValue = false) {
|
|
78
|
+
this.ensureInitialized();
|
|
79
|
+
this.metrics?.counter('forge.feature-flags.checkFlag', this.tags).incr();
|
|
80
|
+
const flag = this.flags.get(flagId);
|
|
81
|
+
if (!flag) {
|
|
82
|
+
return defaultValue;
|
|
83
|
+
}
|
|
84
|
+
const result = this.evaluator.evaluate(user, flag);
|
|
85
|
+
return result.value;
|
|
86
|
+
}
|
|
87
|
+
getFlag(flagId) {
|
|
88
|
+
this.metrics?.counter('forge.feature-flags.getFlag', this.tags).incr();
|
|
89
|
+
return this.flags.get(flagId);
|
|
90
|
+
}
|
|
91
|
+
getAllFlagIds() {
|
|
92
|
+
this.metrics?.counter('forge.feature-flags.getAllFlagIds', this.tags).incr();
|
|
93
|
+
return Array.from(this.flags.keys());
|
|
94
|
+
}
|
|
95
|
+
async refresh() {
|
|
96
|
+
this.metrics?.counter('forge.feature-flags.refresh', this.tags).incr();
|
|
97
|
+
await this.fetchConfigurations();
|
|
98
|
+
}
|
|
99
|
+
startPolling() {
|
|
100
|
+
this.stopPolling();
|
|
101
|
+
this.pollingTimer = setInterval(async () => {
|
|
102
|
+
try {
|
|
103
|
+
await this.fetchConfigurations();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.error('Error during polling:', error);
|
|
107
|
+
}
|
|
108
|
+
}, this.pollingIntervalMs);
|
|
109
|
+
}
|
|
110
|
+
stopPolling() {
|
|
111
|
+
if (this.pollingTimer) {
|
|
112
|
+
clearInterval(this.pollingTimer);
|
|
113
|
+
this.pollingTimer = undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
ensureInitialized() {
|
|
117
|
+
if (!this.initialized) {
|
|
118
|
+
throw new Error('SDK not initialized. Call initialize() first.');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
isInitialized() {
|
|
122
|
+
return this.initialized;
|
|
123
|
+
}
|
|
124
|
+
getLastFetchTime() {
|
|
125
|
+
this.metrics?.counter('forge.feature-flags.getLastFetchTime', this.tags).incr();
|
|
126
|
+
return this.lastFetchTime;
|
|
127
|
+
}
|
|
128
|
+
shutdown() {
|
|
129
|
+
this.stopPolling();
|
|
130
|
+
this.flags.clear();
|
|
131
|
+
this.initialized = false;
|
|
132
|
+
this.metrics?.counter('forge.feature-flags.shutdown', this.tags).incr();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.FeatureFlags = FeatureFlags;
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,cAAc,SAAS,CAAC"}
|
package/out/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeatureFlags = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
var featureFlags_1 = require("./featureFlags");
|
|
6
|
+
Object.defineProperty(exports, "FeatureFlags", { enumerable: true, get: function () { return featureFlags_1.FeatureFlags; } });
|
|
7
|
+
tslib_1.__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export interface ForgeFeatureFlagConfig {
|
|
2
|
+
environment: Environment;
|
|
3
|
+
}
|
|
4
|
+
export interface User {
|
|
5
|
+
attributes?: Record<string, string | number>;
|
|
6
|
+
identifiers?: {
|
|
7
|
+
installContext?: string;
|
|
8
|
+
accountId?: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface Counter {
|
|
12
|
+
incr(): void;
|
|
13
|
+
decr(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface TimerStop {
|
|
16
|
+
stop(): void;
|
|
17
|
+
}
|
|
18
|
+
export interface Timer {
|
|
19
|
+
measure(): TimerStop;
|
|
20
|
+
}
|
|
21
|
+
export interface Metrics {
|
|
22
|
+
counter(name: string, tags?: Record<string, string>): Counter;
|
|
23
|
+
timing(name: string, tags?: Record<string, string>): Timer;
|
|
24
|
+
}
|
|
25
|
+
export declare type Environment = 'development' | 'staging' | 'production';
|
|
26
|
+
export declare type IdType = 'installContext' | 'accountId';
|
|
27
|
+
export declare type FlagStatus = 'implementing' | 'enabled' | 'disabled' | 'launching' | 'tidying';
|
|
28
|
+
export declare type ConditionType = 'everyone' | 'attribute' | 'segment' | 'custom';
|
|
29
|
+
export declare enum FeatureFlagConditionOperator {
|
|
30
|
+
anyOf = "is any of",
|
|
31
|
+
noneOf = "is none of",
|
|
32
|
+
contains = "contains",
|
|
33
|
+
equals = "is equal to",
|
|
34
|
+
gt = "is greater than",
|
|
35
|
+
lt = "is less than",
|
|
36
|
+
version_gt = "is version greater than",
|
|
37
|
+
version_lt = "is version less than"
|
|
38
|
+
}
|
|
39
|
+
export declare type RuleConditionOperator = 'AND' | 'OR';
|
|
40
|
+
export interface ReturnValue {
|
|
41
|
+
type: string;
|
|
42
|
+
value: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface Condition {
|
|
45
|
+
type: string;
|
|
46
|
+
order: number;
|
|
47
|
+
field?: string;
|
|
48
|
+
operator?: string;
|
|
49
|
+
values?: string | string[];
|
|
50
|
+
}
|
|
51
|
+
export interface Rule {
|
|
52
|
+
name: string;
|
|
53
|
+
order: number;
|
|
54
|
+
passPercentage?: number;
|
|
55
|
+
conditions: Condition[];
|
|
56
|
+
env: string[];
|
|
57
|
+
returnValue: ReturnValue;
|
|
58
|
+
}
|
|
59
|
+
export interface Override {
|
|
60
|
+
attribute: string;
|
|
61
|
+
values: string[];
|
|
62
|
+
env: string[];
|
|
63
|
+
order: number;
|
|
64
|
+
returnValue: ReturnValue;
|
|
65
|
+
}
|
|
66
|
+
export interface FeatureFlag {
|
|
67
|
+
flagId: string;
|
|
68
|
+
name: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
status?: string;
|
|
71
|
+
isEnabled: boolean;
|
|
72
|
+
rules: Rule[];
|
|
73
|
+
idType: string;
|
|
74
|
+
createdBy: string;
|
|
75
|
+
createdAt: number;
|
|
76
|
+
updatedAt: number;
|
|
77
|
+
updatedBy?: string;
|
|
78
|
+
overrides?: Override[];
|
|
79
|
+
}
|
|
80
|
+
export interface EvaluationResult {
|
|
81
|
+
value: boolean;
|
|
82
|
+
ruleId: string | null;
|
|
83
|
+
reason: 'override' | 'rule_match' | 'default' | 'disabled';
|
|
84
|
+
}
|
|
85
|
+
export interface EvaluatedFlag {
|
|
86
|
+
value: boolean;
|
|
87
|
+
ruleId: string | null;
|
|
88
|
+
}
|
|
89
|
+
export interface InitializeResponse {
|
|
90
|
+
flags: Record<string, EvaluatedFlag>;
|
|
91
|
+
timestamp: number;
|
|
92
|
+
hash: string;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeatureFlagConditionOperator = void 0;
|
|
4
|
+
var FeatureFlagConditionOperator;
|
|
5
|
+
(function (FeatureFlagConditionOperator) {
|
|
6
|
+
FeatureFlagConditionOperator["anyOf"] = "is any of";
|
|
7
|
+
FeatureFlagConditionOperator["noneOf"] = "is none of";
|
|
8
|
+
FeatureFlagConditionOperator["contains"] = "contains";
|
|
9
|
+
FeatureFlagConditionOperator["equals"] = "is equal to";
|
|
10
|
+
FeatureFlagConditionOperator["gt"] = "is greater than";
|
|
11
|
+
FeatureFlagConditionOperator["lt"] = "is less than";
|
|
12
|
+
FeatureFlagConditionOperator["version_gt"] = "is version greater than";
|
|
13
|
+
FeatureFlagConditionOperator["version_lt"] = "is version less than";
|
|
14
|
+
})(FeatureFlagConditionOperator = exports.FeatureFlagConditionOperator || (exports.FeatureFlagConditionOperator = {}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditions.d.ts","sourceRoot":"","sources":["../../src/utils/conditions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgC,IAAI,EAAE,MAAM,UAAU,CAAC;AAKzE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAe3E"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evaluateCondition = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
5
|
+
function evaluateCondition(user, condition) {
|
|
6
|
+
switch (condition.type) {
|
|
7
|
+
case 'everyone':
|
|
8
|
+
return true;
|
|
9
|
+
case 'attribute':
|
|
10
|
+
case 'custom_field': {
|
|
11
|
+
if (!condition.field || !condition.operator) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const userValue = user.attributes?.[condition.field];
|
|
15
|
+
return evaluateOperator(userValue, condition.values || [], condition.operator);
|
|
16
|
+
}
|
|
17
|
+
default:
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.evaluateCondition = evaluateCondition;
|
|
22
|
+
function evaluateOperator(userValue, targetValues, operator) {
|
|
23
|
+
switch (operator) {
|
|
24
|
+
case types_1.FeatureFlagConditionOperator.anyOf:
|
|
25
|
+
if (Array.isArray(targetValues)) {
|
|
26
|
+
return targetValues.some((tv) => String(tv) === String(userValue));
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
case types_1.FeatureFlagConditionOperator.noneOf:
|
|
30
|
+
if (Array.isArray(targetValues)) {
|
|
31
|
+
return !targetValues.some((tv) => String(tv) === String(userValue));
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
case types_1.FeatureFlagConditionOperator.contains:
|
|
35
|
+
if (Array.isArray(targetValues)) {
|
|
36
|
+
return targetValues.some((v) => String(userValue).includes(String(v)));
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
case types_1.FeatureFlagConditionOperator.equals:
|
|
40
|
+
return String(userValue) === String(targetValues);
|
|
41
|
+
case types_1.FeatureFlagConditionOperator.gt: {
|
|
42
|
+
const userNum = Number(userValue);
|
|
43
|
+
const targetNum = Number(targetValues);
|
|
44
|
+
if (Number.isNaN(userNum) || Number.isNaN(targetNum)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return userNum > targetNum;
|
|
48
|
+
}
|
|
49
|
+
case types_1.FeatureFlagConditionOperator.lt: {
|
|
50
|
+
const userNum = Number(userValue);
|
|
51
|
+
const targetNum = Number(targetValues);
|
|
52
|
+
if (Number.isNaN(userNum) || Number.isNaN(targetNum)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return userNum < targetNum;
|
|
56
|
+
}
|
|
57
|
+
case types_1.FeatureFlagConditionOperator.version_gt:
|
|
58
|
+
return compareVersions(String(userValue), String(targetValues)) > 0;
|
|
59
|
+
case types_1.FeatureFlagConditionOperator.version_lt:
|
|
60
|
+
return compareVersions(String(userValue), String(targetValues)) < 0;
|
|
61
|
+
default:
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function compareVersions(version1, version2) {
|
|
66
|
+
const v1Parts = version1.split('.').map(Number);
|
|
67
|
+
const v2Parts = version2.split('.').map(Number);
|
|
68
|
+
const maxLength = Math.max(v1Parts.length, v2Parts.length);
|
|
69
|
+
for (let i = 0; i < maxLength; i++) {
|
|
70
|
+
const v1Part = v1Parts[i] || 0;
|
|
71
|
+
const v2Part = v2Parts[i] || 0;
|
|
72
|
+
if (v1Part > v2Part)
|
|
73
|
+
return 1;
|
|
74
|
+
if (v1Part < v2Part)
|
|
75
|
+
return -1;
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../src/utils/hash.ts"],"names":[],"mappings":"AAQA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQjD"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeHash = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
function computeHash(input) {
|
|
6
|
+
const hash = (0, node_crypto_1.createHash)('sha256').update(input).digest();
|
|
7
|
+
const hashValue = hash.readBigUInt64BE(0);
|
|
8
|
+
return hashValue;
|
|
9
|
+
}
|
|
10
|
+
exports.computeHash = computeHash;
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forge/feature-flags",
|
|
3
|
+
"version": "0.0.0-experimental-e2ec7ba",
|
|
4
|
+
"description": "Feature Flags Node SDK for Atlassian Forge apps running on Node Runtime",
|
|
5
|
+
"author": "Atlassian",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
7
|
+
"main": "out/index.js",
|
|
8
|
+
"types": "out/index.d.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "yarn run clean && yarn run compile",
|
|
11
|
+
"clean": "rm -rf ./out && rm -f tsconfig.tsbuildinfo",
|
|
12
|
+
"compile": "tsc -b -v"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "20.19.1"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@forge/api": "^7.0.2-next.0-experimental-e2ec7ba"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"registry": "https://packages.atlassian.com/api/npm/npm-public/"
|
|
22
|
+
}
|
|
23
|
+
}
|