@forge/feature-flags-node 1.1.3 → 2.0.0-next.0-experimental-3cf1031

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # @forge/feature-flags-node
2
2
 
3
+ ## 2.0.0-next.0-experimental-3cf1031
4
+
5
+ ### Major Changes
6
+
7
+ - 96f7508: Added new identifiers for targeting
8
+
9
+ ### Patch Changes
10
+
11
+ - @forge/api@6.1.3-next.0-experimental-3cf1031
12
+
13
+ ## 2.0.0-next.0
14
+
15
+ ### Major Changes
16
+
17
+ - 96f7508: Added new identifiers for targeting
18
+
19
+ ### Patch Changes
20
+
21
+ - @forge/api@6.1.3-next.0
22
+
3
23
  ## 1.1.3
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -22,13 +22,15 @@ export const handler = async (payload, context) => {
22
22
  // Initialize the feature flags service
23
23
  const featureFlags = new ForgeFeatureFlags();
24
24
  await featureFlags.initialize({
25
- environment: environmentType?.toLowerCase() || "development" // Optional, defaults to 'development'
25
+ environment: environmentType?.toLowerCase() || "development"
26
26
  });
27
27
 
28
28
  // Define a user
29
29
  const user = {
30
- userID: context?.principal?.accountId,
31
- custom: {
30
+ identifiers:{
31
+ accountId: context?.principal?.accountId,
32
+ },
33
+ attributes: {
32
34
  license: context?.license?.isActive
33
35
  }
34
36
  };
@@ -62,6 +64,16 @@ export const handler = async (payload, context) => {
62
64
  environment: getAppContext().environmentType?.toLowerCase() || "development"
63
65
  });
64
66
 
67
+ // Define a user
68
+ const user = {
69
+ identifiers:{
70
+ installContext: context?.installContext,
71
+ },
72
+ custom: {
73
+ issues: 4
74
+ }
75
+ };
76
+
65
77
  // Use feature flags...
66
78
  const isEnabled = featureFlags.checkFlag(user, "new-feature");
67
79
 
@@ -132,8 +144,12 @@ Checks if the service is initialized.
132
144
 
133
145
  ```typescript
134
146
  interface ForgeUser {
135
- userID: string;
147
+ identifiers?: {
148
+ installContext?: string;
149
+ accountId?: string;
150
+ };
136
151
  custom?: Record<string, string | number | boolean>;
152
+ attributes?: Record<string, string | number | boolean>;
137
153
  }
138
154
  ```
139
155
 
@@ -3,6 +3,8 @@ export declare class ForgeDataAdapter implements IDataAdapter {
3
3
  private readonly pollingIntervalMs;
4
4
  private readonly pollingEnabled;
5
5
  private readonly cache;
6
+ private metrics;
7
+ private tags;
6
8
  constructor(pollingIntervalMs?: number, pollingEnabled?: boolean);
7
9
  get(key: string): Promise<AdapterResponse>;
8
10
  private fetchFromAtlassianServers;
@@ -1 +1 @@
1
- {"version":3,"file":"data-adapter.d.ts","sourceRoot":"","sources":["../src/data-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAK7D,qBAAa,gBAAiB,YAAW,YAAY;IAIjD,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc;IAJjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;gBAGrC,iBAAiB,GAAE,MAAc,EACjC,cAAc,GAAE,OAAc;IAM3C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;YA+ClC,yBAAyB;IAiCjC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU9C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ/B,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IA8B/C,oBAAoB,IAAI,MAAM;CAG/B"}
1
+ {"version":3,"file":"data-adapter.d.ts","sourceRoot":"","sources":["../src/data-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAM7D,qBAAa,gBAAiB,YAAW,YAAY;IASjD,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc;IATjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IACxD,OAAO,CAAC,OAAO,CAAgE;IAC/E,OAAO,CAAC,IAAI,CAGV;gBAGiB,iBAAiB,GAAE,MAAc,EACjC,cAAc,GAAE,OAAc;IAM3C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;YAoDlC,yBAAyB;IAuCjC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU9C,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ/B,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IA8B/C,oBAAoB,IAAI,MAAM;CAG/B"}
@@ -6,16 +6,23 @@ class ForgeDataAdapter {
6
6
  pollingIntervalMs;
7
7
  pollingEnabled;
8
8
  cache = new Map();
9
+ metrics = (0, api_1.__getRuntime)()?.metrics;
10
+ tags = {
11
+ environment: (0, api_1.__getRuntime)()?.appContext?.environmentType || 'unknown',
12
+ appId: (0, api_1.__getRuntime)()?.appContext?.appId || 'unknown'
13
+ };
9
14
  constructor(pollingIntervalMs = 60000, pollingEnabled = true) {
10
15
  this.pollingIntervalMs = pollingIntervalMs;
11
16
  this.pollingEnabled = pollingEnabled;
12
17
  }
13
18
  async get(key) {
14
19
  try {
20
+ this.metrics?.counter('forge.feature-flags.data-adapter.get', this.tags).incr();
15
21
  const data = await this.fetchFromAtlassianServers();
16
22
  if (data) {
17
23
  const serializedData = JSON.stringify(data);
18
24
  this.cache.set(key, serializedData);
25
+ this.metrics?.counter('forge.feature-flags.data-adapter.get.success', this.tags).incr();
19
26
  return {
20
27
  result: serializedData,
21
28
  time: Date.now()
@@ -23,19 +30,23 @@ class ForgeDataAdapter {
23
30
  }
24
31
  const cachedValue = this.cache.get(key);
25
32
  if (cachedValue) {
33
+ this.metrics?.counter('forge.feature-flags.data-adapter.get.cached', this.tags).incr();
26
34
  return {
27
35
  result: cachedValue,
28
36
  time: Date.now()
29
37
  };
30
38
  }
39
+ this.metrics?.counter('forge.feature-flags.data-adapter.get.empty', this.tags).incr();
31
40
  return {
32
41
  result: undefined,
33
42
  time: Date.now()
34
43
  };
35
44
  }
36
45
  catch {
46
+ this.metrics?.counter('forge.feature-flags.data-adapter.get.failure', this.tags).incr();
37
47
  const cachedValue = this.cache.get(key);
38
48
  if (cachedValue) {
49
+ this.metrics?.counter('forge.feature-flags.data-adapter.get.cached', this.tags).incr();
39
50
  return {
40
51
  result: cachedValue,
41
52
  time: Date.now()
@@ -49,11 +60,17 @@ class ForgeDataAdapter {
49
60
  }
50
61
  async fetchFromAtlassianServers() {
51
62
  const runtime = (0, api_1.__getRuntime)();
52
- if (runtime.proxy?.tokenExpiry && runtime.proxy.tokenExpiry * 1000 < Date.now()) {
63
+ if (runtime?.proxy?.tokenExpiry && runtime?.proxy?.tokenExpiry * 1000 < Date.now()) {
64
+ this.metrics
65
+ ?.counter('forge.feature-flags.data-adapter.fetchFromAtlassianServers.tokenExpired', this.tags)
66
+ .incr();
53
67
  return null;
54
68
  }
55
- const remainingTime = runtime.lambdaContext?.getRemainingTimeInMillis?.();
69
+ const remainingTime = runtime?.lambdaContext?.getRemainingTimeInMillis?.();
56
70
  if (remainingTime !== undefined && remainingTime < 5000) {
71
+ this.metrics
72
+ ?.counter('forge.feature-flags.data-adapter.fetchFromAtlassianServers.remainingTimeComplete', this.tags)
73
+ .incr();
57
74
  return null;
58
75
  }
59
76
  const response = await (0, api_1.__fetchProduct)({ provider: 'app', remote: 'feature-flags', type: 'feature-flags' })('/', {
@@ -84,10 +101,10 @@ class ForgeDataAdapter {
84
101
  }
85
102
  try {
86
103
  const runtime = (0, api_1.__getRuntime)();
87
- if (runtime.proxy?.tokenExpiry && runtime.proxy.tokenExpiry * 1000 < Date.now()) {
104
+ if (runtime?.proxy?.tokenExpiry && runtime?.proxy?.tokenExpiry * 1000 < Date.now()) {
88
105
  return false;
89
106
  }
90
- const remainingTime = runtime.lambdaContext?.getRemainingTimeInMillis?.();
107
+ const remainingTime = runtime?.lambdaContext?.getRemainingTimeInMillis?.();
91
108
  if (remainingTime !== undefined && remainingTime < 10000) {
92
109
  return false;
93
110
  }
package/out/index.d.ts CHANGED
@@ -2,12 +2,33 @@ export interface ForgeFeatureFlagConfig {
2
2
  environment?: 'development' | 'staging' | 'production';
3
3
  }
4
4
  export interface ForgeUser {
5
- userID: string;
5
+ userID?: string;
6
6
  custom?: Record<string, string | number | boolean>;
7
+ attributes?: Record<string, string | number | boolean>;
8
+ identifiers?: {
9
+ installContext?: string;
10
+ accountId?: string;
11
+ };
12
+ }
13
+ export interface Counter {
14
+ incr(): void;
15
+ decr(): void;
16
+ }
17
+ export interface TimerStop {
18
+ stop(): void;
19
+ }
20
+ export interface Timer {
21
+ measure(): TimerStop;
22
+ }
23
+ export interface Metrics {
24
+ counter(name: string, tags?: Record<string, string>): Counter;
25
+ timing(name: string, tags?: Record<string, string>): Timer;
7
26
  }
8
27
  export declare class ForgeFeatureFlags {
9
28
  private initialized;
10
29
  private dataAdapter;
30
+ private metrics;
31
+ private tags;
11
32
  initialize(config?: ForgeFeatureFlagConfig): Promise<void>;
12
33
  checkFlag(user: ForgeUser, flagName: string): boolean;
13
34
  getFeatureFlags(user: ForgeUser, flagNames: string[]): Record<string, boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,YAAY,CAAC;CACxD;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CACpD;AAKD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAiC;IAKvC,UAAU,CAAC,MAAM,GAAE,sBAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BpE,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAQrD,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAcxE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAe/B,aAAa,IAAI,OAAO;IAI/B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,WAAW;CAMpB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,YAAY,CAAC;CACxD;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;IACvD,WAAW,CAAC,EAAE;QACZ,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,IAAI,IAAI,CAAC;IACb,IAAI,IAAI,IAAI,CAAC;CACd;AACD,MAAM,WAAW,SAAS;IACxB,IAAI,IAAI,IAAI,CAAC;CACd;AACD,MAAM,WAAW,KAAK;IACpB,OAAO,IAAI,SAAS,CAAC;CACtB;AACD,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC;IAC9D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC;CAC5D;AAKD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,IAAI,CAGV;IAIW,UAAU,CAAC,MAAM,GAAE,sBAA2B,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCpE,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IASrD,eAAe,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAexE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAe/B,aAAa,IAAI,OAAO;IAI/B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,WAAW;CAOpB"}
package/out/index.js CHANGED
@@ -4,35 +4,54 @@ exports.ForgeFeatureFlags = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const statsig_node_1 = tslib_1.__importDefault(require("statsig-node"));
6
6
  const data_adapter_1 = require("./data-adapter");
7
+ const api_1 = require("@forge/api");
7
8
  class ForgeFeatureFlags {
8
9
  initialized = false;
9
10
  dataAdapter = null;
11
+ metrics = (0, api_1.__getRuntime)()?.metrics;
12
+ tags = {
13
+ environment: (0, api_1.__getRuntime)()?.appContext?.environmentType || 'unknown',
14
+ appId: (0, api_1.__getRuntime)()?.appContext?.appId || 'unknown'
15
+ };
10
16
  async initialize(config = {}) {
11
17
  if (this.initialized) {
12
18
  return;
13
19
  }
20
+ this.metrics?.counter('forge.feature-flags.initialize', this.tags).incr();
21
+ const timer = this.metrics?.timing('forge.feature-flags.initialize', this.tags).measure();
14
22
  try {
15
23
  statsig_node_1.default.shutdown();
16
24
  }
17
25
  catch { }
26
+ const remainingTime = (0, api_1.__getRuntime)()?.lambdaContext?.getRemainingTimeInMillis?.();
27
+ const disableRulesetsSync = remainingTime && remainingTime < 60000 ? true : false;
28
+ if (disableRulesetsSync) {
29
+ this.metrics?.counter('forge.feature-flags.disableRulesetsSync', this.tags).incr();
30
+ }
18
31
  this.dataAdapter = new data_adapter_1.ForgeDataAdapter(60000, true);
19
32
  const statsigOptions = {
20
33
  environment: { tier: config.environment || 'development' },
21
- disableRulesetsSync: false,
34
+ disableRulesetsSync: disableRulesetsSync,
22
35
  disableIdListsSync: true,
23
36
  initTimeoutMs: 3000,
24
37
  localMode: true,
38
+ disableAllLogging: true,
39
+ disableDiagnostics: true,
25
40
  dataAdapter: this.dataAdapter
26
41
  };
27
- await statsig_node_1.default.initialize('forge-internal-key', statsigOptions);
42
+ await statsig_node_1.default.initialize('secret-forge-internal-key', statsigOptions);
28
43
  this.initialized = true;
44
+ timer?.stop();
45
+ this.metrics?.counter('forge.feature-flags.initialize.success', this.tags).incr();
29
46
  }
30
47
  checkFlag(user, flagName) {
31
48
  this.ensureInitialized();
49
+ this.metrics?.counter('forge.feature-flags.checkFlag', this.tags).incr();
32
50
  return statsig_node_1.default.checkGate(this.convertUser(user), flagName);
33
51
  }
34
52
  getFeatureFlags(user, flagNames) {
35
53
  this.ensureInitialized();
54
+ this.metrics?.counter('forge.feature-flags.getFeatureFlags', this.tags).incr();
36
55
  const results = {};
37
56
  for (const flagName of flagNames) {
38
57
  results[flagName] = statsig_node_1.default.checkGate(this.convertUser(user), flagName);
@@ -48,6 +67,7 @@ class ForgeFeatureFlags {
48
67
  await this.dataAdapter.shutdown();
49
68
  }
50
69
  this.initialized = false;
70
+ this.metrics?.counter('forge.feature-flags.shutdown', this.tags).incr();
51
71
  }
52
72
  isInitialized() {
53
73
  return this.initialized;
@@ -60,7 +80,8 @@ class ForgeFeatureFlags {
60
80
  convertUser(user) {
61
81
  return {
62
82
  userID: user.userID,
63
- custom: user.custom
83
+ custom: { ...(user.custom || {}), ...(user.attributes || {}) },
84
+ customIDs: user.identifiers || {}
64
85
  };
65
86
  }
66
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forge/feature-flags-node",
3
- "version": "1.1.3",
3
+ "version": "2.0.0-next.0-experimental-3cf1031",
4
4
  "description": "Feature Flags Node SDK for Atlassian Forge apps running on Node Runtime",
5
5
  "author": "Atlassian",
6
6
  "license": "SEE LICENSE IN LICENSE.txt",
@@ -15,7 +15,7 @@
15
15
  "@types/node": "20.19.1"
16
16
  },
17
17
  "dependencies": {
18
- "@forge/api": "^6.1.2",
18
+ "@forge/api": "^6.1.3-next.0-experimental-3cf1031",
19
19
  "statsig-node": "^6.4.3"
20
20
  },
21
21
  "publishConfig": {