@budibase/backend-core 2.30.2 → 2.30.4
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/index.js +371 -169
- package/dist/index.js.map +3 -3
- package/dist/index.js.meta.json +1 -1
- package/dist/package.json +5 -5
- package/dist/plugins.js +1 -0
- package/dist/plugins.js.map +2 -2
- package/dist/plugins.js.meta.json +1 -1
- package/dist/src/context/mainContext.d.ts +2 -0
- package/dist/src/context/mainContext.js +19 -0
- package/dist/src/context/mainContext.js.map +1 -1
- package/dist/src/context/types.d.ts +3 -0
- package/dist/src/environment.d.ts +4 -0
- package/dist/src/environment.js +27 -1
- package/dist/src/environment.js.map +1 -1
- package/dist/src/events/processors/posthog/PosthogProcessor.d.ts +1 -1
- package/dist/src/events/processors/posthog/PosthogProcessor.js +2 -2
- package/dist/src/events/processors/posthog/PosthogProcessor.js.map +1 -1
- package/dist/src/features/index.d.ts +30 -28
- package/dist/src/features/index.js +200 -79
- package/dist/src/features/index.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/redis/redis.d.ts +1 -0
- package/dist/src/redis/redis.js +4 -0
- package/dist/src/redis/redis.js.map +1 -1
- package/dist/src/security/auth.js +1 -1
- package/dist/src/security/auth.js.map +1 -1
- package/dist/src/sql/sqlTable.js +23 -8
- package/dist/src/sql/sqlTable.js.map +1 -1
- package/dist/tests/core/utilities/mocks/index.d.ts +0 -2
- package/dist/tests/core/utilities/mocks/index.js +1 -7
- package/dist/tests/core/utilities/mocks/index.js.map +1 -1
- package/dist/tests/core/utilities/structures/users.js +1 -1
- package/dist/tests/core/utilities/structures/users.js.map +1 -1
- package/dist/tests/jestSetup.js +7 -2
- package/dist/tests/jestSetup.js.map +1 -1
- package/package.json +5 -5
- package/src/context/mainContext.ts +19 -0
- package/src/context/tests/index.spec.ts +1 -1
- package/src/context/types.ts +3 -0
- package/src/environment.ts +29 -0
- package/src/events/processors/posthog/PosthogProcessor.ts +1 -1
- package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +16 -22
- package/src/features/index.ts +242 -80
- package/src/features/tests/features.spec.ts +209 -63
- package/src/index.ts +1 -1
- package/src/middleware/passport/sso/tests/oidc.spec.ts +4 -12
- package/src/middleware/passport/sso/tests/sso.spec.ts +10 -12
- package/src/plugin/tests/validation.spec.ts +168 -42
- package/src/redis/redis.ts +4 -0
- package/src/redis/tests/redis.spec.ts +6 -3
- package/src/security/auth.ts +1 -1
- package/src/security/tests/auth.spec.ts +2 -2
- package/src/sql/sqlTable.ts +21 -7
- package/tests/core/utilities/mocks/index.ts +0 -2
- package/tests/core/utilities/structures/users.ts +1 -1
- package/tests/jestSetup.ts +10 -3
- package/dist/tests/core/utilities/mocks/fetch.d.ts +0 -32
- package/dist/tests/core/utilities/mocks/fetch.js +0 -15
- package/dist/tests/core/utilities/mocks/fetch.js.map +0 -1
- package/dist/tests/core/utilities/mocks/posthog.d.ts +0 -0
- package/dist/tests/core/utilities/mocks/posthog.js +0 -9
- package/dist/tests/core/utilities/mocks/posthog.js.map +0 -1
- package/tests/core/utilities/mocks/fetch.ts +0 -17
- package/tests/core/utilities/mocks/posthog.ts +0 -7
package/src/features/index.ts
CHANGED
|
@@ -1,108 +1,270 @@
|
|
|
1
1
|
import env from "../environment"
|
|
2
2
|
import * as context from "../context"
|
|
3
|
-
import {
|
|
3
|
+
import { PostHog, PostHogOptions } from "posthog-node"
|
|
4
|
+
import { IdentityType, UserCtx } from "@budibase/types"
|
|
5
|
+
import tracer from "dd-trace"
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
let posthog: PostHog | undefined
|
|
8
|
+
export function init(opts?: PostHogOptions) {
|
|
9
|
+
if (env.POSTHOG_TOKEN && env.POSTHOG_API_HOST) {
|
|
10
|
+
console.log("initializing posthog client...")
|
|
11
|
+
posthog = new PostHog(env.POSTHOG_TOKEN, {
|
|
12
|
+
host: env.POSTHOG_API_HOST,
|
|
13
|
+
personalApiKey: env.POSTHOG_PERSONAL_TOKEN,
|
|
14
|
+
...opts,
|
|
15
|
+
})
|
|
16
|
+
} else {
|
|
17
|
+
console.log("posthog disabled")
|
|
8
18
|
}
|
|
9
|
-
|
|
10
|
-
private constructor(public defaultValue: T) {}
|
|
11
19
|
}
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// All of the machinery in this file is to make sure that flags have their
|
|
16
|
-
// default values set correctly and their types flow through the system.
|
|
17
|
-
const FLAGS = {
|
|
18
|
-
LICENSING: Flag.withDefault(false),
|
|
19
|
-
GOOGLE_SHEETS: Flag.withDefault(false),
|
|
20
|
-
USER_GROUPS: Flag.withDefault(false),
|
|
21
|
-
ONBOARDING_TOUR: Flag.withDefault(false),
|
|
21
|
+
export function shutdown() {
|
|
22
|
+
posthog?.shutdown()
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
export abstract class Flag<T> {
|
|
26
|
+
static boolean(defaultValue: boolean): Flag<boolean> {
|
|
27
|
+
return new BooleanFlag(defaultValue)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static string(defaultValue: string): Flag<string> {
|
|
31
|
+
return new StringFlag(defaultValue)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static number(defaultValue: number): Flag<number> {
|
|
35
|
+
return new NumberFlag(defaultValue)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected constructor(public defaultValue: T) {}
|
|
39
|
+
|
|
40
|
+
abstract parse(value: any): T
|
|
41
|
+
}
|
|
30
42
|
|
|
31
43
|
type UnwrapFlag<F> = F extends Flag<infer U> ? U : never
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
|
|
45
|
+
export type FlagValues<T> = {
|
|
46
|
+
[K in keyof T]: UnwrapFlag<T[K]>
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
type KeysOfType<T, U> = {
|
|
50
|
+
[K in keyof T]: T[K] extends Flag<U> ? K : never
|
|
51
|
+
}[keyof T]
|
|
52
|
+
|
|
53
|
+
class BooleanFlag extends Flag<boolean> {
|
|
54
|
+
parse(value: any) {
|
|
55
|
+
if (typeof value === "string") {
|
|
56
|
+
return ["true", "t", "1"].includes(value.toLowerCase())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof value === "boolean") {
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error(`could not parse value "${value}" as boolean`)
|
|
64
|
+
}
|
|
39
65
|
}
|
|
40
66
|
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
class StringFlag extends Flag<string> {
|
|
68
|
+
parse(value: any) {
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
return value
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`could not parse value "${value}" as string`)
|
|
73
|
+
}
|
|
43
74
|
}
|
|
44
75
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
* Check the tests for examples of how TENANT_FEATURE_FLAGS should be formatted.
|
|
51
|
-
*
|
|
52
|
-
* In future we plan to add more ways of setting feature flags, e.g. PostHog, and
|
|
53
|
-
* they will be accessed through this function as well.
|
|
54
|
-
*/
|
|
55
|
-
export async function fetch(): Promise<Flags> {
|
|
56
|
-
const currentTenantId = context.getTenantId()
|
|
57
|
-
const flags = defaultFlags()
|
|
58
|
-
|
|
59
|
-
const split = (env.TENANT_FEATURE_FLAGS || "")
|
|
60
|
-
.split(",")
|
|
61
|
-
.map(x => x.split(":"))
|
|
62
|
-
for (const [tenantId, ...features] of split) {
|
|
63
|
-
if (!tenantId || (tenantId !== "*" && tenantId !== currentTenantId)) {
|
|
64
|
-
continue
|
|
76
|
+
class NumberFlag extends Flag<number> {
|
|
77
|
+
parse(value: any) {
|
|
78
|
+
if (typeof value === "number") {
|
|
79
|
+
return value
|
|
65
80
|
}
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
value = false
|
|
82
|
+
if (typeof value === "string") {
|
|
83
|
+
const parsed = parseFloat(value)
|
|
84
|
+
if (!isNaN(parsed)) {
|
|
85
|
+
return parsed
|
|
72
86
|
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new Error(`could not parse value "${value}" as number`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|
94
|
+
// This is used to safely cache flags sets in the current request context.
|
|
95
|
+
// Because multiple sets could theoretically exist, we don't want the cache of
|
|
96
|
+
// one to leak into another.
|
|
97
|
+
private readonly setId: string
|
|
98
|
+
|
|
99
|
+
constructor(private readonly flagSchema: T) {
|
|
100
|
+
this.setId = crypto.randomUUID()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
defaults(): FlagValues<T> {
|
|
104
|
+
return Object.keys(this.flagSchema).reduce((acc, key) => {
|
|
105
|
+
const typedKey = key as keyof T
|
|
106
|
+
acc[typedKey] = this.flagSchema[key].defaultValue
|
|
107
|
+
return acc
|
|
108
|
+
}, {} as FlagValues<T>)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
isFlagName(name: string | number | symbol): name is keyof T {
|
|
112
|
+
return this.flagSchema[name as keyof T] !== undefined
|
|
113
|
+
}
|
|
73
114
|
|
|
74
|
-
|
|
75
|
-
|
|
115
|
+
async get<K extends keyof T>(
|
|
116
|
+
key: K,
|
|
117
|
+
ctx?: UserCtx
|
|
118
|
+
): Promise<FlagValues<T>[K]> {
|
|
119
|
+
const flags = await this.fetch(ctx)
|
|
120
|
+
return flags[key]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async isEnabled<K extends KeysOfType<T, boolean>>(
|
|
124
|
+
key: K,
|
|
125
|
+
ctx?: UserCtx
|
|
126
|
+
): Promise<boolean> {
|
|
127
|
+
const flags = await this.fetch(ctx)
|
|
128
|
+
return flags[key]
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async fetch(ctx?: UserCtx): Promise<FlagValues<T>> {
|
|
132
|
+
return await tracer.trace("features.fetch", async span => {
|
|
133
|
+
const cachedFlags = context.getFeatureFlags<FlagValues<T>>(this.setId)
|
|
134
|
+
if (cachedFlags) {
|
|
135
|
+
span?.addTags({ fromCache: true })
|
|
136
|
+
return cachedFlags
|
|
76
137
|
}
|
|
77
138
|
|
|
78
|
-
|
|
79
|
-
|
|
139
|
+
const tags: Record<string, any> = {}
|
|
140
|
+
const flagValues = this.defaults()
|
|
141
|
+
const currentTenantId = context.getTenantId()
|
|
142
|
+
const specificallySetFalse = new Set<string>()
|
|
143
|
+
|
|
144
|
+
const split = (env.TENANT_FEATURE_FLAGS || "")
|
|
145
|
+
.split(",")
|
|
146
|
+
.map(x => x.split(":"))
|
|
147
|
+
for (const [tenantId, ...features] of split) {
|
|
148
|
+
if (!tenantId || (tenantId !== "*" && tenantId !== currentTenantId)) {
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
tags[`readFromEnvironmentVars`] = true
|
|
153
|
+
|
|
154
|
+
for (let feature of features) {
|
|
155
|
+
let value = true
|
|
156
|
+
if (feature.startsWith("!")) {
|
|
157
|
+
feature = feature.slice(1)
|
|
158
|
+
value = false
|
|
159
|
+
specificallySetFalse.add(feature)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ignore unknown flags
|
|
163
|
+
if (!this.isFlagName(feature)) {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (typeof flagValues[feature] !== "boolean") {
|
|
168
|
+
throw new Error(`Feature: ${feature} is not a boolean`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// @ts-expect-error - TS does not like you writing into a generic type,
|
|
172
|
+
// but we know that it's okay in this case because it's just an object.
|
|
173
|
+
flagValues[feature as keyof FlagValues] = value
|
|
174
|
+
tags[`flags.${feature}.source`] = "environment"
|
|
175
|
+
}
|
|
80
176
|
}
|
|
81
177
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
178
|
+
const license = ctx?.user?.license
|
|
179
|
+
if (license) {
|
|
180
|
+
tags[`readFromLicense`] = true
|
|
86
181
|
|
|
87
|
-
|
|
88
|
-
|
|
182
|
+
for (const feature of license.features) {
|
|
183
|
+
if (!this.isFlagName(feature)) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
89
186
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
187
|
+
if (
|
|
188
|
+
flagValues[feature] === true ||
|
|
189
|
+
specificallySetFalse.has(feature)
|
|
190
|
+
) {
|
|
191
|
+
// If the flag is already set to through environment variables, we
|
|
192
|
+
// don't want to override it back to false here.
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// @ts-expect-error - TS does not like you writing into a generic type,
|
|
197
|
+
// but we know that it's okay in this case because it's just an object.
|
|
198
|
+
flagValues[feature] = true
|
|
199
|
+
tags[`flags.${feature}.source`] = "license"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
96
202
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
203
|
+
const identity = context.getIdentity()
|
|
204
|
+
tags[`identity.type`] = identity?.type
|
|
205
|
+
tags[`identity.tenantId`] = identity?.tenantId
|
|
206
|
+
tags[`identity._id`] = identity?._id
|
|
207
|
+
|
|
208
|
+
if (posthog && identity?.type === IdentityType.USER) {
|
|
209
|
+
tags[`readFromPostHog`] = true
|
|
210
|
+
|
|
211
|
+
const personProperties: Record<string, string> = {}
|
|
212
|
+
if (identity.tenantId) {
|
|
213
|
+
personProperties.tenantId = identity.tenantId
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const posthogFlags = await posthog.getAllFlagsAndPayloads(
|
|
217
|
+
identity._id,
|
|
218
|
+
{
|
|
219
|
+
personProperties,
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
|
|
224
|
+
if (!this.isFlagName(name)) {
|
|
225
|
+
// We don't want an unexpected PostHog flag to break the app, so we
|
|
226
|
+
// just log it and continue.
|
|
227
|
+
console.warn(`Unexpected posthog flag "${name}": ${value}`)
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (flagValues[name] === true || specificallySetFalse.has(name)) {
|
|
232
|
+
// If the flag is already set to through environment variables, we
|
|
233
|
+
// don't want to override it back to false here.
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const payload = posthogFlags.featureFlagPayloads?.[name]
|
|
238
|
+
const flag = this.flagSchema[name]
|
|
239
|
+
try {
|
|
240
|
+
// @ts-expect-error - TS does not like you writing into a generic
|
|
241
|
+
// type, but we know that it's okay in this case because it's just
|
|
242
|
+
// an object.
|
|
243
|
+
flagValues[name] = flag.parse(payload || value)
|
|
244
|
+
tags[`flags.${name}.source`] = "posthog"
|
|
245
|
+
} catch (err) {
|
|
246
|
+
// We don't want an invalid PostHog flag to break the app, so we just
|
|
247
|
+
// log it and continue.
|
|
248
|
+
console.warn(`Error parsing posthog flag "${name}": ${value}`, err)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
context.setFeatureFlags(this.setId, flagValues)
|
|
254
|
+
for (const [key, value] of Object.entries(flagValues)) {
|
|
255
|
+
tags[`flags.${key}.value`] = value
|
|
256
|
+
}
|
|
257
|
+
span?.addTags(tags)
|
|
258
|
+
|
|
259
|
+
return flagValues
|
|
260
|
+
})
|
|
261
|
+
}
|
|
108
262
|
}
|
|
263
|
+
|
|
264
|
+
// This is the primary source of truth for feature flags. If you want to add a
|
|
265
|
+
// new flag, add it here and use the `fetch` and `get` functions to access it.
|
|
266
|
+
// All of the machinery in this file is to make sure that flags have their
|
|
267
|
+
// default values set correctly and their types flow through the system.
|
|
268
|
+
export const flags = new FlagSet({
|
|
269
|
+
DEFAULT_VALUES: Flag.boolean(false),
|
|
270
|
+
})
|
|
@@ -1,86 +1,232 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { IdentityContext, IdentityType, UserCtx } from "@budibase/types"
|
|
2
|
+
import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
|
|
3
|
+
import * as context from "../../context"
|
|
4
|
+
import environment, { withEnv } from "../../environment"
|
|
5
|
+
import nodeFetch from "node-fetch"
|
|
6
|
+
import nock from "nock"
|
|
7
|
+
|
|
8
|
+
const schema = {
|
|
9
|
+
TEST_BOOLEAN: Flag.boolean(false),
|
|
10
|
+
TEST_STRING: Flag.string("default value"),
|
|
11
|
+
TEST_NUMBER: Flag.number(0),
|
|
12
|
+
}
|
|
13
|
+
const flags = new FlagSet(schema)
|
|
14
|
+
|
|
15
|
+
interface TestCase {
|
|
16
|
+
it: string
|
|
17
|
+
identity?: Partial<IdentityContext>
|
|
18
|
+
environmentFlags?: string
|
|
19
|
+
posthogFlags?: PostHogFlags
|
|
20
|
+
licenseFlags?: Array<string>
|
|
21
|
+
expected?: Partial<FlagValues<typeof schema>>
|
|
22
|
+
errorMessage?: string | RegExp
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PostHogFlags {
|
|
26
|
+
featureFlags?: Record<string, boolean>
|
|
27
|
+
featureFlagPayloads?: Record<string, string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mockPosthogFlags(flags: PostHogFlags) {
|
|
31
|
+
nock("https://us.i.posthog.com")
|
|
32
|
+
.post("/decide/?v=3", body => {
|
|
33
|
+
return body.token === "test" && body.distinct_id === "us_1234"
|
|
34
|
+
})
|
|
35
|
+
.reply(200, flags)
|
|
36
|
+
.persist()
|
|
13
37
|
}
|
|
14
38
|
|
|
15
39
|
describe("feature flags", () => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
expected: Partial<Flags>
|
|
20
|
-
}
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
nock.cleanAll()
|
|
42
|
+
})
|
|
21
43
|
|
|
22
44
|
it.each<TestCase>([
|
|
23
45
|
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
expected: {
|
|
46
|
+
it: "should should find a simple boolean flag in the environment",
|
|
47
|
+
environmentFlags: "default:TEST_BOOLEAN",
|
|
48
|
+
expected: { TEST_BOOLEAN: true },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
it: "should should find a simple netgative boolean flag in the environment",
|
|
52
|
+
environmentFlags: "default:!TEST_BOOLEAN",
|
|
53
|
+
expected: { TEST_BOOLEAN: false },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
it: "should should match stars in the environment",
|
|
57
|
+
environmentFlags: "*:TEST_BOOLEAN",
|
|
58
|
+
expected: { TEST_BOOLEAN: true },
|
|
27
59
|
},
|
|
28
60
|
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
expected: {
|
|
61
|
+
it: "should not match a different tenant's flags",
|
|
62
|
+
environmentFlags: "otherTenant:TEST_BOOLEAN",
|
|
63
|
+
expected: { TEST_BOOLEAN: false },
|
|
32
64
|
},
|
|
33
65
|
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
expected: { ONBOARDING_TOUR: true },
|
|
66
|
+
it: "should return the defaults when no flags are set",
|
|
67
|
+
expected: flags.defaults(),
|
|
37
68
|
},
|
|
38
69
|
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
expected: {
|
|
70
|
+
it: "should ignore unknown feature flags",
|
|
71
|
+
environmentFlags: "default:TEST_BOOLEAN,default:FOO",
|
|
72
|
+
expected: { TEST_BOOLEAN: true },
|
|
42
73
|
},
|
|
43
74
|
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
it: "should be able to read boolean flags from PostHog",
|
|
76
|
+
posthogFlags: {
|
|
77
|
+
featureFlags: { TEST_BOOLEAN: true },
|
|
78
|
+
},
|
|
79
|
+
expected: { TEST_BOOLEAN: true },
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
it: "should be able to read string flags from PostHog",
|
|
83
|
+
posthogFlags: {
|
|
84
|
+
featureFlags: { TEST_STRING: true },
|
|
85
|
+
featureFlagPayloads: { TEST_STRING: "test" },
|
|
86
|
+
},
|
|
87
|
+
expected: { TEST_STRING: "test" },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
it: "should be able to read numeric flags from PostHog",
|
|
91
|
+
posthogFlags: {
|
|
92
|
+
featureFlags: { TEST_NUMBER: true },
|
|
93
|
+
featureFlagPayloads: { TEST_NUMBER: "123" },
|
|
94
|
+
},
|
|
95
|
+
expected: { TEST_NUMBER: 123 },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
it: "should not be able to override a negative environment flag from PostHog",
|
|
99
|
+
environmentFlags: "default:!TEST_BOOLEAN",
|
|
100
|
+
posthogFlags: {
|
|
101
|
+
featureFlags: { TEST_BOOLEAN: true },
|
|
102
|
+
},
|
|
103
|
+
expected: { TEST_BOOLEAN: false },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
it: "should not be able to override a positive environment flag from PostHog",
|
|
107
|
+
environmentFlags: "default:TEST_BOOLEAN",
|
|
108
|
+
posthogFlags: {
|
|
109
|
+
featureFlags: {
|
|
110
|
+
TEST_BOOLEAN: false,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
expected: { TEST_BOOLEAN: true },
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
it: "should be able to set boolean flags through the license",
|
|
117
|
+
licenseFlags: ["TEST_BOOLEAN"],
|
|
118
|
+
expected: { TEST_BOOLEAN: true },
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
it: "should not be able to override a negative environment flag from license",
|
|
122
|
+
environmentFlags: "default:!TEST_BOOLEAN",
|
|
123
|
+
licenseFlags: ["TEST_BOOLEAN"],
|
|
124
|
+
expected: { TEST_BOOLEAN: false },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
it: "should not error on unrecognised PostHog flag",
|
|
128
|
+
posthogFlags: {
|
|
129
|
+
featureFlags: { UNDEFINED: true },
|
|
130
|
+
},
|
|
131
|
+
expected: flags.defaults(),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
it: "should not error on unrecognised license flag",
|
|
135
|
+
licenseFlags: ["UNDEFINED"],
|
|
136
|
+
expected: flags.defaults(),
|
|
47
137
|
},
|
|
48
138
|
])(
|
|
49
|
-
|
|
50
|
-
({
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
139
|
+
"$it",
|
|
140
|
+
async ({
|
|
141
|
+
identity,
|
|
142
|
+
environmentFlags,
|
|
143
|
+
posthogFlags,
|
|
144
|
+
licenseFlags,
|
|
145
|
+
expected,
|
|
146
|
+
errorMessage,
|
|
147
|
+
}) => {
|
|
148
|
+
const env: Partial<typeof environment> = {
|
|
149
|
+
TENANT_FEATURE_FLAGS: environmentFlags,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (posthogFlags) {
|
|
153
|
+
mockPosthogFlags(posthogFlags)
|
|
154
|
+
env.POSTHOG_TOKEN = "test"
|
|
155
|
+
env.POSTHOG_API_HOST = "https://us.i.posthog.com"
|
|
156
|
+
env.POSTHOG_PERSONAL_TOKEN = "test"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const ctx = { user: { license: { features: licenseFlags || [] } } }
|
|
160
|
+
|
|
161
|
+
await withEnv(env, async () => {
|
|
162
|
+
// We need to pass in node-fetch here otherwise nock won't get used
|
|
163
|
+
// because posthog-node uses axios under the hood.
|
|
164
|
+
init({
|
|
165
|
+
fetch: (url, opts) => {
|
|
166
|
+
return nodeFetch(url, opts)
|
|
167
|
+
},
|
|
60
168
|
})
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
169
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
170
|
+
const fullIdentity: IdentityContext = {
|
|
171
|
+
_id: "us_1234",
|
|
172
|
+
tenantId: "default",
|
|
173
|
+
type: IdentityType.USER,
|
|
174
|
+
email: "test@example.com",
|
|
175
|
+
firstName: "Test",
|
|
176
|
+
lastName: "User",
|
|
177
|
+
...identity,
|
|
178
|
+
}
|
|
69
179
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
180
|
+
await context.doInIdentityContext(fullIdentity, async () => {
|
|
181
|
+
if (errorMessage) {
|
|
182
|
+
await expect(flags.fetch(ctx as UserCtx)).rejects.toThrow(
|
|
183
|
+
errorMessage
|
|
184
|
+
)
|
|
185
|
+
} else if (expected) {
|
|
186
|
+
const values = await flags.fetch(ctx as UserCtx)
|
|
187
|
+
expect(values).toMatchObject(expected)
|
|
188
|
+
|
|
189
|
+
for (const [key, expectedValue] of Object.entries(expected)) {
|
|
190
|
+
const value = await flags.get(
|
|
191
|
+
key as keyof typeof schema,
|
|
192
|
+
ctx as UserCtx
|
|
193
|
+
)
|
|
194
|
+
expect(value).toBe(expectedValue)
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
throw new Error("No expected value")
|
|
198
|
+
}
|
|
82
199
|
})
|
|
83
|
-
|
|
200
|
+
|
|
201
|
+
shutdown()
|
|
202
|
+
})
|
|
84
203
|
}
|
|
85
204
|
)
|
|
205
|
+
|
|
206
|
+
it("should not error if PostHog is down", async () => {
|
|
207
|
+
const identity: IdentityContext = {
|
|
208
|
+
_id: "us_1234",
|
|
209
|
+
tenantId: "default",
|
|
210
|
+
type: IdentityType.USER,
|
|
211
|
+
email: "test@example.com",
|
|
212
|
+
firstName: "Test",
|
|
213
|
+
lastName: "User",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
nock("https://us.i.posthog.com")
|
|
217
|
+
.post("/decide/?v=3", body => {
|
|
218
|
+
return body.token === "test" && body.distinct_id === "us_1234"
|
|
219
|
+
})
|
|
220
|
+
.reply(503)
|
|
221
|
+
.persist()
|
|
222
|
+
|
|
223
|
+
await withEnv(
|
|
224
|
+
{ POSTHOG_TOKEN: "test", POSTHOG_API_HOST: "https://us.i.posthog.com" },
|
|
225
|
+
async () => {
|
|
226
|
+
await context.doInIdentityContext(identity, async () => {
|
|
227
|
+
await flags.fetch()
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
})
|
|
86
232
|
})
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ export * as locks from "./redis/redlockImpl"
|
|
|
27
27
|
export * as utils from "./utils"
|
|
28
28
|
export * as errors from "./errors"
|
|
29
29
|
export * as timers from "./timers"
|
|
30
|
-
export { default as env } from "./environment"
|
|
30
|
+
export { default as env, withEnv, setEnv } from "./environment"
|
|
31
31
|
export * as blacklist from "./blacklist"
|
|
32
32
|
export * as docUpdates from "./docUpdates"
|
|
33
33
|
export * from "./utils/Duration"
|