@terreno/feature-flags 57.16.2 → 57.17.0

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/evaluate.js CHANGED
@@ -6,7 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.evaluateAllFlags = exports.evaluateFlag = exports.deterministicHash = void 0;
7
7
  const api_1 = require("@terreno/api");
8
8
  const get_1 = __importDefault(require("lodash/get"));
9
- const isDebugEnabled = () => process.env.FEATURE_FLAGS_DEBUG !== "false";
9
+ // Evaluation diagnostics are opt-in and never include the user's attribute values.
10
+ const isDebugEnabled = () => process.env.FEATURE_FLAGS_DEBUG === "true";
10
11
  /**
11
12
  * Deterministic hash of a string to a number between 0 and 99.
12
13
  * Uses a simple but effective hash (djb2) to ensure the same user+flag
@@ -58,7 +59,7 @@ const matchesFieldRule = (user, rule) => {
58
59
  break;
59
60
  }
60
61
  if (isDebugEnabled()) {
61
- api_1.logger.info(`[feature-flags] field rule: user.${rule.field}=${JSON.stringify(fieldValue)} ${rule.operator} ${JSON.stringify(ruleValue)} → ${result}`);
62
+ api_1.logger.debug(`[feature-flags] field rule: user.${rule.field} ${rule.operator} → ${result}`);
62
63
  }
63
64
  return result;
64
65
  };
@@ -72,7 +73,7 @@ const matchesRule = (user, rule, segments) => {
72
73
  try {
73
74
  const result = segmentFn(user);
74
75
  if (isDebugEnabled()) {
75
- api_1.logger.info(`[feature-flags] segment rule: "${rule.segment}" → ${result}`);
76
+ api_1.logger.debug(`[feature-flags] segment rule: "${rule.segment}" → ${result}`);
76
77
  }
77
78
  return result;
78
79
  }
@@ -91,7 +92,7 @@ const evaluateFlag = (flag, userId, user, segments) => {
91
92
  const debug = isDebugEnabled();
92
93
  if (!flag.enabled) {
93
94
  if (debug) {
94
- api_1.logger.info(`[feature-flags] "${flag.key}" is disabled → ${flag.type === "variant" ? "null" : "false"}`);
95
+ api_1.logger.debug(`[feature-flags] "${flag.key}" is disabled → ${flag.type === "variant" ? "null" : "false"}`);
95
96
  }
96
97
  return flag.type === "variant" ? null : false;
97
98
  }
@@ -102,7 +103,7 @@ const evaluateFlag = (flag, userId, user, segments) => {
102
103
  if (matched) {
103
104
  const result = flag.type === "variant" ? (rule.variant ?? null) : (rule.enabled ?? false);
104
105
  if (debug) {
105
- api_1.logger.info(`[feature-flags] "${flag.key}" matched rule ${i} → ${JSON.stringify(result)}`);
106
+ api_1.logger.debug(`[feature-flags] "${flag.key}" matched rule ${i} → ${JSON.stringify(result)}`);
106
107
  }
107
108
  return result;
108
109
  }
@@ -112,7 +113,7 @@ const evaluateFlag = (flag, userId, user, segments) => {
112
113
  if (flag.type === "boolean") {
113
114
  const result = hash < flag.rolloutPercentage;
114
115
  if (debug) {
115
- api_1.logger.info(`[feature-flags] "${flag.key}" no rules matched, hash=${hash} rollout=${flag.rolloutPercentage}% → ${result}`);
116
+ api_1.logger.debug(`[feature-flags] "${flag.key}" no rules matched, hash=${hash} rollout=${flag.rolloutPercentage}% → ${result}`);
116
117
  }
117
118
  return result;
118
119
  }
@@ -122,7 +123,7 @@ const evaluateFlag = (flag, userId, user, segments) => {
122
123
  cumulativeWeight += variant.weight;
123
124
  if (hash < cumulativeWeight) {
124
125
  if (debug) {
125
- api_1.logger.info(`[feature-flags] "${flag.key}" no rules matched, hash=${hash} → variant "${variant.key}"`);
126
+ api_1.logger.debug(`[feature-flags] "${flag.key}" no rules matched, hash=${hash} → variant "${variant.key}"`);
126
127
  }
127
128
  return variant.key;
128
129
  }
@@ -130,7 +131,7 @@ const evaluateFlag = (flag, userId, user, segments) => {
130
131
  // Fallback (shouldn't happen if weights sum to 100)
131
132
  const fallback = flag.variants.length > 0 ? flag.variants[flag.variants.length - 1].key : null;
132
133
  if (debug) {
133
- api_1.logger.info(`[feature-flags] "${flag.key}" variant fallback → ${JSON.stringify(fallback)}`);
134
+ api_1.logger.debug(`[feature-flags] "${flag.key}" variant fallback → ${JSON.stringify(fallback)}`);
134
135
  }
135
136
  return fallback;
136
137
  };
@@ -143,13 +144,13 @@ const evaluateAllFlags = async (flagModel, userId, user, segments) => {
143
144
  const flags = await flagModel.find({ archived: { $ne: true }, enabled: true });
144
145
  const results = {};
145
146
  if (isDebugEnabled()) {
146
- api_1.logger.info(`[feature-flags] evaluating ${flags.length} flags for user ${userId}`);
147
+ api_1.logger.debug(`[feature-flags] evaluating ${flags.length} flags for user ${userId}`);
147
148
  }
148
149
  for (const flag of flags) {
149
150
  results[flag.key] = (0, exports.evaluateFlag)(flag, userId, user, segments);
150
151
  }
151
152
  if (isDebugEnabled()) {
152
- api_1.logger.info(`[feature-flags] results: ${JSON.stringify(results)}`);
153
+ api_1.logger.debug(`[feature-flags] results: ${JSON.stringify(results)}`);
153
154
  }
154
155
  return results;
155
156
  };
package/package.json CHANGED
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "dependencies": {
7
7
  "@openfeature/server-sdk": "^1.22.0",
8
- "@terreno/api": "57.16.2",
8
+ "@terreno/api": "57.17.0",
9
9
  "express": "^5.2.1",
10
10
  "lodash": "^4.18.1"
11
11
  },
12
12
  "description": "Feature flags and A/B testing plugin for @terreno/api — part of Terreno, the batteries-included TypeScript framework for universal apps.",
13
13
  "devDependencies": {
14
14
  "@biomejs/biome": "^2.3.6",
15
- "@terreno/test": "57.16.2",
15
+ "@terreno/test": "57.17.0",
16
16
  "@types/bun": "^1.4.0",
17
17
  "@types/express": "^5.0.6",
18
18
  "@types/lodash": "^4.17.17",
@@ -53,5 +53,5 @@
53
53
  "test:coverage": "bun run ../scripts/check-coverage.ts"
54
54
  },
55
55
  "types": "dist/index.d.ts",
56
- "version": "57.16.2"
56
+ "version": "57.17.0"
57
57
  }
package/src/evaluate.ts CHANGED
@@ -7,7 +7,8 @@ import type {
7
7
  SegmentFunction,
8
8
  } from "./types";
9
9
 
10
- const isDebugEnabled = (): boolean => process.env.FEATURE_FLAGS_DEBUG !== "false";
10
+ // Evaluation diagnostics are opt-in and never include the user's attribute values.
11
+ const isDebugEnabled = (): boolean => process.env.FEATURE_FLAGS_DEBUG === "true";
11
12
 
12
13
  /**
13
14
  * Deterministic hash of a string to a number between 0 and 99.
@@ -63,9 +64,7 @@ const matchesFieldRule = (user: unknown, rule: FeatureFlagRule): boolean => {
63
64
  }
64
65
 
65
66
  if (isDebugEnabled()) {
66
- logger.info(
67
- `[feature-flags] field rule: user.${rule.field}=${JSON.stringify(fieldValue)} ${rule.operator} ${JSON.stringify(ruleValue)} → ${result}`
68
- );
67
+ logger.debug(`[feature-flags] field rule: user.${rule.field} ${rule.operator} → ${result}`);
69
68
  }
70
69
 
71
70
  return result;
@@ -85,7 +84,7 @@ const matchesRule = (
85
84
  try {
86
85
  const result = segmentFn(user);
87
86
  if (isDebugEnabled()) {
88
- logger.info(`[feature-flags] segment rule: "${rule.segment}" → ${result}`);
87
+ logger.debug(`[feature-flags] segment rule: "${rule.segment}" → ${result}`);
89
88
  }
90
89
  return result;
91
90
  } catch (err) {
@@ -111,7 +110,7 @@ export const evaluateFlag = (
111
110
 
112
111
  if (!flag.enabled) {
113
112
  if (debug) {
114
- logger.info(
113
+ logger.debug(
115
114
  `[feature-flags] "${flag.key}" is disabled → ${flag.type === "variant" ? "null" : "false"}`
116
115
  );
117
116
  }
@@ -125,7 +124,7 @@ export const evaluateFlag = (
125
124
  if (matched) {
126
125
  const result = flag.type === "variant" ? (rule.variant ?? null) : (rule.enabled ?? false);
127
126
  if (debug) {
128
- logger.info(`[feature-flags] "${flag.key}" matched rule ${i} → ${JSON.stringify(result)}`);
127
+ logger.debug(`[feature-flags] "${flag.key}" matched rule ${i} → ${JSON.stringify(result)}`);
129
128
  }
130
129
  return result;
131
130
  }
@@ -137,7 +136,7 @@ export const evaluateFlag = (
137
136
  if (flag.type === "boolean") {
138
137
  const result = hash < flag.rolloutPercentage;
139
138
  if (debug) {
140
- logger.info(
139
+ logger.debug(
141
140
  `[feature-flags] "${flag.key}" no rules matched, hash=${hash} rollout=${flag.rolloutPercentage}% → ${result}`
142
141
  );
143
142
  }
@@ -150,7 +149,7 @@ export const evaluateFlag = (
150
149
  cumulativeWeight += variant.weight;
151
150
  if (hash < cumulativeWeight) {
152
151
  if (debug) {
153
- logger.info(
152
+ logger.debug(
154
153
  `[feature-flags] "${flag.key}" no rules matched, hash=${hash} → variant "${variant.key}"`
155
154
  );
156
155
  }
@@ -161,7 +160,7 @@ export const evaluateFlag = (
161
160
  // Fallback (shouldn't happen if weights sum to 100)
162
161
  const fallback = flag.variants.length > 0 ? flag.variants[flag.variants.length - 1].key : null;
163
162
  if (debug) {
164
- logger.info(`[feature-flags] "${flag.key}" variant fallback → ${JSON.stringify(fallback)}`);
163
+ logger.debug(`[feature-flags] "${flag.key}" variant fallback → ${JSON.stringify(fallback)}`);
165
164
  }
166
165
  return fallback;
167
166
  };
@@ -180,7 +179,7 @@ export const evaluateAllFlags = async (
180
179
  const results: EvaluationResult = {};
181
180
 
182
181
  if (isDebugEnabled()) {
183
- logger.info(`[feature-flags] evaluating ${flags.length} flags for user ${userId}`);
182
+ logger.debug(`[feature-flags] evaluating ${flags.length} flags for user ${userId}`);
184
183
  }
185
184
 
186
185
  for (const flag of flags) {
@@ -188,7 +187,7 @@ export const evaluateAllFlags = async (
188
187
  }
189
188
 
190
189
  if (isDebugEnabled()) {
191
- logger.info(`[feature-flags] results: ${JSON.stringify(results)}`);
190
+ logger.debug(`[feature-flags] results: ${JSON.stringify(results)}`);
192
191
  }
193
192
 
194
193
  return results;
@@ -1,4 +1,5 @@
1
- import {describe, expect, it} from "bun:test";
1
+ import {afterEach, describe, expect, it} from "bun:test";
2
+ import {logger} from "@terreno/api";
2
3
  import {deterministicHash, evaluateAllFlags, evaluateFlag} from "../evaluate";
3
4
  import type {FeatureFlagDocument, FeatureFlagModel, SegmentFunction} from "../types";
4
5
 
@@ -515,6 +516,60 @@ describe("evaluateFlag", () => {
515
516
  });
516
517
  });
517
518
 
519
+ describe("evaluation diagnostics", () => {
520
+ const originalDebugEnv = process.env.FEATURE_FLAGS_DEBUG;
521
+ const originalInfo = logger.info;
522
+ const originalDebug = logger.debug;
523
+
524
+ const captureLogs = (): string[] => {
525
+ const lines: string[] = [];
526
+ logger.info = ((message: string) => {
527
+ lines.push(message);
528
+ }) as typeof logger.info;
529
+ logger.debug = ((message: string) => {
530
+ lines.push(message);
531
+ }) as typeof logger.debug;
532
+ return lines;
533
+ };
534
+
535
+ const emailRuleFlag = (): FeatureFlagDocument =>
536
+ createFlag({
537
+ key: "staff-only",
538
+ rolloutPercentage: 0,
539
+ rules: [{enabled: true, field: "email", operator: "contains", value: "@staff.example"}],
540
+ });
541
+
542
+ afterEach(() => {
543
+ logger.info = originalInfo;
544
+ logger.debug = originalDebug;
545
+ if (originalDebugEnv === undefined) {
546
+ delete process.env.FEATURE_FLAGS_DEBUG;
547
+ } else {
548
+ process.env.FEATURE_FLAGS_DEBUG = originalDebugEnv;
549
+ }
550
+ });
551
+
552
+ it("logs nothing about a user's attributes by default", () => {
553
+ delete process.env.FEATURE_FLAGS_DEBUG;
554
+ const lines = captureLogs();
555
+
556
+ evaluateFlag(emailRuleFlag(), "user1", {email: "dummy-user@example.com"}, noSegments);
557
+
558
+ expect(lines.join("\n")).not.toContain("dummy-user@example.com");
559
+ });
560
+
561
+ it("never logs the user's attribute value when debug logging is on", () => {
562
+ process.env.FEATURE_FLAGS_DEBUG = "true";
563
+ const lines = captureLogs();
564
+
565
+ evaluateFlag(emailRuleFlag(), "user1", {email: "dummy-user@example.com"}, noSegments);
566
+
567
+ const combined = lines.join("\n");
568
+ expect(combined).not.toContain("dummy-user@example.com");
569
+ expect(combined).toContain("user.email contains");
570
+ });
571
+ });
572
+
518
573
  describe("evaluateAllFlags", () => {
519
574
  const makeMockModel = (flags: FeatureFlagDocument[]): FeatureFlagModel => {
520
575
  return {