@budibase/backend-core 2.30.2 → 2.30.3
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 +267 -89
- 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.meta.json +1 -1
- 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 +29 -26
- package/dist/src/features/index.js +195 -79
- package/dist/src/features/index.js.map +1 -1
- package/dist/src/index.d.ts +1 -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/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 +231 -81
- package/src/features/tests/features.spec.ts +204 -60
- 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 +5 -2
- 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
|
@@ -25,19 +25,13 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
25
25
|
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
26
26
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
27
27
|
};
|
|
28
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
29
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
30
|
-
};
|
|
31
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.
|
|
29
|
+
exports.licenses = exports.date = exports.accounts = void 0;
|
|
33
30
|
jest.mock("../../../../src/accounts");
|
|
34
31
|
const _accounts = __importStar(require("../../../../src/accounts"));
|
|
35
32
|
exports.accounts = jest.mocked(_accounts);
|
|
36
33
|
exports.date = __importStar(require("./date"));
|
|
37
34
|
exports.licenses = __importStar(require("./licenses"));
|
|
38
|
-
var fetch_1 = require("./fetch");
|
|
39
|
-
Object.defineProperty(exports, "fetch", { enumerable: true, get: function () { return __importDefault(fetch_1).default; } });
|
|
40
35
|
__exportStar(require("./alerts"), exports);
|
|
41
36
|
require("./events");
|
|
42
|
-
require("./posthog");
|
|
43
37
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../tests/core/utilities/mocks/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../tests/core/utilities/mocks/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAI,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;AACrC,oEAAqD;AAExC,QAAA,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;AAE9C,+CAA8B;AAC9B,uDAAsC;AACtC,2CAAwB;AACxB,oBAAiB"}
|
|
@@ -12,7 +12,7 @@ const newEmail = () => {
|
|
|
12
12
|
exports.newEmail = newEmail;
|
|
13
13
|
const user = (userProps) => {
|
|
14
14
|
const userId = userProps === null || userProps === void 0 ? void 0 : userProps._id;
|
|
15
|
-
return Object.assign({ _id: userId, userId, email: (0, exports.newEmail)(), password: "
|
|
15
|
+
return Object.assign({ _id: userId, userId, email: (0, exports.newEmail)(), password: "password123!", roles: { app_test: "admin" }, firstName: generator_1.generator.first(), lastName: generator_1.generator.last(), pictureUrl: "http://example.com", tenantId: _1.tenant.id() }, userProps);
|
|
16
16
|
};
|
|
17
17
|
exports.user = user;
|
|
18
18
|
const adminUser = (userProps) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../../../../tests/core/utilities/structures/users.ts"],"names":[],"mappings":";;;AAwEA,0BAqBC;AArFD,+BAAmC;AACnC,qCAA+B;AAC/B,2CAAuC;AACvC,wBAA0B;AAEnB,MAAM,QAAQ,GAAG,GAAG,EAAE;IAC3B,OAAO,GAAG,IAAA,aAAI,GAAE,cAAc,CAAA;AAChC,CAAC,CAAA;AAFY,QAAA,QAAQ,YAEpB;AAEM,MAAM,IAAI,GAAG,CAAC,SAAyC,EAAQ,EAAE;IACtE,MAAM,MAAM,GAAG,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,GAAG,CAAA;IAC7B,uBACE,GAAG,EAAE,MAAM,EACX,MAAM,EACN,KAAK,EAAE,IAAA,gBAAQ,GAAE,EACjB,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../../../../tests/core/utilities/structures/users.ts"],"names":[],"mappings":";;;AAwEA,0BAqBC;AArFD,+BAAmC;AACnC,qCAA+B;AAC/B,2CAAuC;AACvC,wBAA0B;AAEnB,MAAM,QAAQ,GAAG,GAAG,EAAE;IAC3B,OAAO,GAAG,IAAA,aAAI,GAAE,cAAc,CAAA;AAChC,CAAC,CAAA;AAFY,QAAA,QAAQ,YAEpB;AAEM,MAAM,IAAI,GAAG,CAAC,SAAyC,EAAQ,EAAE;IACtE,MAAM,MAAM,GAAG,SAAS,aAAT,SAAS,uBAAT,SAAS,CAAE,GAAG,CAAA;IAC7B,uBACE,GAAG,EAAE,MAAM,EACX,MAAM,EACN,KAAK,EAAE,IAAA,gBAAQ,GAAE,EACjB,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,EAC5B,SAAS,EAAE,qBAAS,CAAC,KAAK,EAAE,EAC5B,QAAQ,EAAE,qBAAS,CAAC,IAAI,EAAE,EAC1B,UAAU,EAAE,oBAAoB,EAChC,QAAQ,EAAE,SAAM,CAAC,EAAE,EAAE,IAClB,SAAS,EACb;AACH,CAAC,CAAA;AAdY,QAAA,IAAI,QAchB;AAEM,MAAM,SAAS,GAAG,CAAC,SAAe,EAAa,EAAE;IACtD,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,KAAK,EAAE;YACL,MAAM,EAAE,IAAI;SACb,EACD,OAAO,EAAE;YACP,MAAM,EAAE,IAAI;SACb,IACF;AACH,CAAC,CAAA;AAVY,QAAA,SAAS,aAUrB;AAEM,MAAM,aAAa,GAAG,CAAC,SAAe,EAAiB,EAAE;IAC9D,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,KAAK,EAAE;YACL,MAAM,EAAE,IAAI;SACb,IACF;AACH,CAAC,CAAA;AAPY,QAAA,aAAa,iBAOzB;AAEM,MAAM,WAAW,GAAG,CAAC,SAAyB,EAAe,EAAE;IACpE,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,OAAO,EAAE;YACP,MAAM,EAAE,IAAI;SACb,IACF;AACH,CAAC,CAAA;AAPY,QAAA,WAAW,eAOvB;AAEM,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,SAAe,EAAe,EAAE;IAC5E,uCACK,IAAA,YAAI,EAAC,SAAS,CAAC,KAClB,OAAO,EAAE;YACP,IAAI,EAAE,CAAC,KAAK,CAAC;SACd,IACF;AACH,CAAC,CAAA;AAPY,QAAA,cAAc,kBAO1B;AAED,SAAgB,OAAO,CACrB,OAAiD,EAAE;;IAEnD,MAAM,IAAI,GAAG,IAAA,YAAI,EAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5B,OAAO,IAAI,CAAC,QAAQ,CAAA;IAEpB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,CAAC,OAAO,GAAG,IAAA,iBAAW,EAAC,IAAI,CAAC,CAAA;IAClC,CAAC;IAED,uCACK,IAAI,KACP,kBAAkB,EAAE,KAAK,EACzB,MAAM,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,MAAM,EAC5B,QAAQ,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,QAAS,EACjC,YAAY,EAAE,MAAA,IAAI,CAAC,OAAO,0CAAE,YAAa,EACzC,iBAAiB,EAAE;YACjB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,OAAO,EAAE,IAAI,CAAC,UAAU;SACzB,IACF;AACH,CAAC"}
|
package/dist/tests/jestSetup.js
CHANGED
|
@@ -7,11 +7,16 @@ require("./core/logging");
|
|
|
7
7
|
const environment_1 = __importDefault(require("../src/environment"));
|
|
8
8
|
const timers_1 = require("../src/timers");
|
|
9
9
|
const utilities_1 = require("./core/utilities");
|
|
10
|
-
|
|
11
|
-
utilities_1.mocks.fetch.enable();
|
|
10
|
+
const nock_1 = __importDefault(require("nock"));
|
|
12
11
|
// mock all dates to 2020-01-01T00:00:00.000Z
|
|
13
12
|
// use tk.reset() to use real dates in individual tests
|
|
14
13
|
const timekeeper_1 = __importDefault(require("timekeeper"));
|
|
14
|
+
nock_1.default.disableNetConnect();
|
|
15
|
+
nock_1.default.enableNetConnect(host => {
|
|
16
|
+
return (host.includes("localhost") ||
|
|
17
|
+
host.includes("127.0.0.1") ||
|
|
18
|
+
host.includes("::1"));
|
|
19
|
+
});
|
|
15
20
|
timekeeper_1.default.freeze(utilities_1.mocks.date.MOCK_DATE);
|
|
16
21
|
if (!process.env.DEBUG) {
|
|
17
22
|
console.log = jest.fn(); // console.log are ignored in tests
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jestSetup.js","sourceRoot":"","sources":["../../tests/jestSetup.ts"],"names":[],"mappings":";;;;;AAAA,0BAAuB;AACvB,qEAAoC;AACpC,0CAAuC;AACvC,gDAA4D;
|
|
1
|
+
{"version":3,"file":"jestSetup.js","sourceRoot":"","sources":["../../tests/jestSetup.ts"],"names":[],"mappings":";;;;;AAAA,0BAAuB;AACvB,qEAAoC;AACpC,0CAAuC;AACvC,gDAA4D;AAC5D,gDAAuB;AAEvB,6CAA6C;AAC7C,uDAAuD;AACvD,4DAA2B;AAE3B,cAAI,CAAC,iBAAiB,EAAE,CAAA;AACxB,cAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE;IAC3B,OAAO,CACL,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC1B,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;QAC1B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CACrB,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,oBAAE,CAAC,MAAM,CAAC,iBAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;AAE/B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IACvB,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,EAAE,CAAA,CAAC,mCAAmC;AAC7D,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;IACpB,4CAA4C;IAC5C,cAAc;IACd,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;AACzB,CAAC;AAED,8BAAkB,CAAC,QAAQ,CAAC,qBAAG,CAAC,CAAA;AAEhC,QAAQ,CAAC,GAAG,EAAE;IACZ,IAAA,gBAAO,GAAE,CAAA;AACX,CAAC,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/backend-core",
|
|
3
|
-
"version": "2.30.
|
|
3
|
+
"version": "2.30.3",
|
|
4
4
|
"description": "Budibase backend core libraries used in server and worker",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@budibase/nano": "10.1.5",
|
|
25
25
|
"@budibase/pouchdb-replication-stream": "1.2.11",
|
|
26
|
-
"@budibase/shared-core": "2.30.
|
|
27
|
-
"@budibase/types": "2.30.
|
|
26
|
+
"@budibase/shared-core": "2.30.3",
|
|
27
|
+
"@budibase/types": "2.30.3",
|
|
28
28
|
"aws-cloudfront-sign": "3.0.2",
|
|
29
29
|
"aws-sdk": "2.1030.0",
|
|
30
30
|
"bcrypt": "5.1.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"passport-oauth2-refresh": "^2.1.0",
|
|
47
47
|
"pino": "8.11.0",
|
|
48
48
|
"pino-http": "8.3.3",
|
|
49
|
-
"posthog-node": "
|
|
49
|
+
"posthog-node": "4.0.1",
|
|
50
50
|
"pouchdb": "7.3.0",
|
|
51
51
|
"pouchdb-find": "7.2.2",
|
|
52
52
|
"redlock": "4.2.0",
|
|
@@ -95,5 +95,5 @@
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
|
-
"gitHead": "
|
|
98
|
+
"gitHead": "bb6092e58b80d4343bf3b720881f2dc01a30d1c9"
|
|
99
99
|
}
|
package/src/environment.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs"
|
|
2
2
|
import { ServiceType } from "@budibase/types"
|
|
3
|
+
import { cloneDeep } from "lodash"
|
|
3
4
|
|
|
4
5
|
function isTest() {
|
|
5
6
|
return isJest()
|
|
@@ -144,6 +145,8 @@ const environment = {
|
|
|
144
145
|
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
|
145
146
|
PLATFORM_URL: process.env.PLATFORM_URL || "",
|
|
146
147
|
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,
|
|
148
|
+
POSTHOG_PERSONAL_TOKEN: process.env.POSTHOG_PERSONAL_TOKEN,
|
|
149
|
+
POSTHOG_API_HOST: process.env.POSTHOG_API_HOST || "https://us.i.posthog.com",
|
|
147
150
|
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
|
|
148
151
|
TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
|
|
149
152
|
CLOUDFRONT_CDN: process.env.CLOUDFRONT_CDN,
|
|
@@ -208,6 +211,32 @@ const environment = {
|
|
|
208
211
|
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
|
209
212
|
}
|
|
210
213
|
|
|
214
|
+
export function setEnv(newEnvVars: Partial<typeof environment>): () => void {
|
|
215
|
+
const oldEnv = cloneDeep(environment)
|
|
216
|
+
|
|
217
|
+
let key: keyof typeof newEnvVars
|
|
218
|
+
for (key in newEnvVars) {
|
|
219
|
+
environment._set(key, newEnvVars[key])
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return () => {
|
|
223
|
+
for (const [key, value] of Object.entries(oldEnv)) {
|
|
224
|
+
environment._set(key, value)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function withEnv<T>(envVars: Partial<typeof environment>, f: () => T) {
|
|
230
|
+
const cleanup = setEnv(envVars)
|
|
231
|
+
const result = f()
|
|
232
|
+
if (result instanceof Promise) {
|
|
233
|
+
return result.finally(cleanup)
|
|
234
|
+
} else {
|
|
235
|
+
cleanup()
|
|
236
|
+
return result
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
211
240
|
type EnvironmentKey = keyof typeof environment
|
|
212
241
|
export const SECRETS: EnvironmentKey[] = [
|
|
213
242
|
"API_ENCRYPTION_KEY",
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { testEnv } from "../../../../../tests/extra"
|
|
2
2
|
import PosthogProcessor from "../PosthogProcessor"
|
|
3
3
|
import { Event, IdentityType, Hosting } from "@budibase/types"
|
|
4
|
-
|
|
5
|
-
const tk = require("timekeeper")
|
|
6
|
-
|
|
4
|
+
import tk from "timekeeper"
|
|
7
5
|
import * as cache from "../../../../cache/generic"
|
|
8
6
|
import { CacheKey } from "../../../../cache/generic"
|
|
9
7
|
import * as context from "../../../../context"
|
|
@@ -18,6 +16,9 @@ const newIdentity = () => {
|
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
describe("PosthogProcessor", () => {
|
|
19
|
+
let processor: PosthogProcessor
|
|
20
|
+
let spy: jest.SpyInstance
|
|
21
|
+
|
|
21
22
|
beforeAll(() => {
|
|
22
23
|
testEnv.singleTenant()
|
|
23
24
|
})
|
|
@@ -27,33 +28,29 @@ describe("PosthogProcessor", () => {
|
|
|
27
28
|
await cache.bustCache(
|
|
28
29
|
`${CacheKey.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}`
|
|
29
30
|
)
|
|
31
|
+
|
|
32
|
+
processor = new PosthogProcessor("test")
|
|
33
|
+
spy = jest.spyOn(processor.posthog, "capture")
|
|
30
34
|
})
|
|
31
35
|
|
|
32
36
|
describe("processEvent", () => {
|
|
33
37
|
it("processes event", async () => {
|
|
34
|
-
const processor = new PosthogProcessor("test")
|
|
35
|
-
|
|
36
38
|
const identity = newIdentity()
|
|
37
39
|
const properties = {}
|
|
38
40
|
|
|
39
41
|
await processor.processEvent(Event.APP_CREATED, identity, properties)
|
|
40
|
-
|
|
41
|
-
expect(processor.posthog.capture).toHaveBeenCalledTimes(1)
|
|
42
|
+
expect(spy).toHaveBeenCalledTimes(1)
|
|
42
43
|
})
|
|
43
44
|
|
|
44
45
|
it("honours exclusions", async () => {
|
|
45
|
-
const processor = new PosthogProcessor("test")
|
|
46
|
-
|
|
47
46
|
const identity = newIdentity()
|
|
48
47
|
const properties = {}
|
|
49
48
|
|
|
50
49
|
await processor.processEvent(Event.AUTH_SSO_UPDATED, identity, properties)
|
|
51
|
-
expect(
|
|
50
|
+
expect(spy).toHaveBeenCalledTimes(0)
|
|
52
51
|
})
|
|
53
52
|
|
|
54
53
|
it("removes audited information", async () => {
|
|
55
|
-
const processor = new PosthogProcessor("test")
|
|
56
|
-
|
|
57
54
|
const identity = newIdentity()
|
|
58
55
|
const properties = {
|
|
59
56
|
email: "test",
|
|
@@ -63,7 +60,8 @@ describe("PosthogProcessor", () => {
|
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
await processor.processEvent(Event.USER_CREATED, identity, properties)
|
|
66
|
-
expect(
|
|
63
|
+
expect(spy).toHaveBeenCalled()
|
|
64
|
+
|
|
67
65
|
// @ts-ignore
|
|
68
66
|
const call = processor.posthog.capture.mock.calls[0][0]
|
|
69
67
|
expect(call.properties.audited).toBeUndefined()
|
|
@@ -72,7 +70,6 @@ describe("PosthogProcessor", () => {
|
|
|
72
70
|
|
|
73
71
|
describe("rate limiting", () => {
|
|
74
72
|
it("sends daily event once in same day", async () => {
|
|
75
|
-
const processor = new PosthogProcessor("test")
|
|
76
73
|
const identity = newIdentity()
|
|
77
74
|
const properties = {}
|
|
78
75
|
|
|
@@ -82,11 +79,10 @@ describe("PosthogProcessor", () => {
|
|
|
82
79
|
tk.freeze(new Date(2022, 0, 1, 15, 0))
|
|
83
80
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
|
84
81
|
|
|
85
|
-
expect(
|
|
82
|
+
expect(spy).toHaveBeenCalledTimes(1)
|
|
86
83
|
})
|
|
87
84
|
|
|
88
85
|
it("sends daily event once per unique day", async () => {
|
|
89
|
-
const processor = new PosthogProcessor("test")
|
|
90
86
|
const identity = newIdentity()
|
|
91
87
|
const properties = {}
|
|
92
88
|
|
|
@@ -102,11 +98,10 @@ describe("PosthogProcessor", () => {
|
|
|
102
98
|
tk.freeze(new Date(2022, 0, 3, 6, 0))
|
|
103
99
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
|
104
100
|
|
|
105
|
-
expect(
|
|
101
|
+
expect(spy).toHaveBeenCalledTimes(3)
|
|
106
102
|
})
|
|
107
103
|
|
|
108
104
|
it("sends event again after cache expires", async () => {
|
|
109
|
-
const processor = new PosthogProcessor("test")
|
|
110
105
|
const identity = newIdentity()
|
|
111
106
|
const properties = {}
|
|
112
107
|
|
|
@@ -120,11 +115,10 @@ describe("PosthogProcessor", () => {
|
|
|
120
115
|
tk.freeze(new Date(2022, 0, 1, 14, 0))
|
|
121
116
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
|
|
122
117
|
|
|
123
|
-
expect(
|
|
118
|
+
expect(spy).toHaveBeenCalledTimes(2)
|
|
124
119
|
})
|
|
125
120
|
|
|
126
121
|
it("sends per app events once per day per app", async () => {
|
|
127
|
-
const processor = new PosthogProcessor("test")
|
|
128
122
|
const identity = newIdentity()
|
|
129
123
|
const properties = {}
|
|
130
124
|
|
|
@@ -160,10 +154,10 @@ describe("PosthogProcessor", () => {
|
|
|
160
154
|
}
|
|
161
155
|
|
|
162
156
|
await runAppEvents("app_1")
|
|
163
|
-
expect(
|
|
157
|
+
expect(spy).toHaveBeenCalledTimes(4)
|
|
164
158
|
|
|
165
159
|
await runAppEvents("app_2")
|
|
166
|
-
expect(
|
|
160
|
+
expect(spy).toHaveBeenCalledTimes(8)
|
|
167
161
|
})
|
|
168
162
|
})
|
|
169
163
|
})
|
package/src/features/index.ts
CHANGED
|
@@ -1,108 +1,258 @@
|
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
export abstract class Flag<T> {
|
|
22
|
+
static boolean(defaultValue: boolean): Flag<boolean> {
|
|
23
|
+
return new BooleanFlag(defaultValue)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static string(defaultValue: string): Flag<string> {
|
|
27
|
+
return new StringFlag(defaultValue)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static number(defaultValue: number): Flag<number> {
|
|
31
|
+
return new NumberFlag(defaultValue)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected constructor(public defaultValue: T) {}
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// @ts-ignore
|
|
27
|
-
acc[typedKey] = FLAGS[typedKey].defaultValue
|
|
28
|
-
return acc
|
|
29
|
-
}, {} as Flags)
|
|
36
|
+
abstract parse(value: any): T
|
|
37
|
+
}
|
|
30
38
|
|
|
31
39
|
type UnwrapFlag<F> = F extends Flag<infer U> ? U : never
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
|
|
41
|
+
export type FlagValues<T> = {
|
|
42
|
+
[K in keyof T]: UnwrapFlag<T[K]>
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
type KeysOfType<T, U> = {
|
|
46
|
+
[K in keyof T]: T[K] extends Flag<U> ? K : never
|
|
47
|
+
}[keyof T]
|
|
48
|
+
|
|
49
|
+
class BooleanFlag extends Flag<boolean> {
|
|
50
|
+
parse(value: any) {
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
return ["true", "t", "1"].includes(value.toLowerCase())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof value === "boolean") {
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error(`could not parse value "${value}" as boolean`)
|
|
60
|
+
}
|
|
39
61
|
}
|
|
40
62
|
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
class StringFlag extends Flag<string> {
|
|
64
|
+
parse(value: any) {
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
return value
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`could not parse value "${value}" as string`)
|
|
69
|
+
}
|
|
43
70
|
}
|
|
44
71
|
|
|
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
|
|
72
|
+
class NumberFlag extends Flag<number> {
|
|
73
|
+
parse(value: any) {
|
|
74
|
+
if (typeof value === "number") {
|
|
75
|
+
return value
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
value = false
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
const parsed = parseFloat(value)
|
|
80
|
+
if (!isNaN(parsed)) {
|
|
81
|
+
return parsed
|
|
72
82
|
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw new Error(`could not parse value "${value}" as number`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
|
90
|
+
constructor(private readonly flagSchema: T) {}
|
|
91
|
+
|
|
92
|
+
defaults(): FlagValues<T> {
|
|
93
|
+
return Object.keys(this.flagSchema).reduce((acc, key) => {
|
|
94
|
+
const typedKey = key as keyof T
|
|
95
|
+
acc[typedKey] = this.flagSchema[key].defaultValue
|
|
96
|
+
return acc
|
|
97
|
+
}, {} as FlagValues<T>)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
isFlagName(name: string | number | symbol): name is keyof T {
|
|
101
|
+
return this.flagSchema[name as keyof T] !== undefined
|
|
102
|
+
}
|
|
73
103
|
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
async get<K extends keyof T>(
|
|
105
|
+
key: K,
|
|
106
|
+
ctx?: UserCtx
|
|
107
|
+
): Promise<FlagValues<T>[K]> {
|
|
108
|
+
const flags = await this.fetch(ctx)
|
|
109
|
+
return flags[key]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async isEnabled<K extends KeysOfType<T, boolean>>(
|
|
113
|
+
key: K,
|
|
114
|
+
ctx?: UserCtx
|
|
115
|
+
): Promise<boolean> {
|
|
116
|
+
const flags = await this.fetch(ctx)
|
|
117
|
+
return flags[key]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async fetch(ctx?: UserCtx): Promise<FlagValues<T>> {
|
|
121
|
+
return await tracer.trace("features.fetch", async span => {
|
|
122
|
+
const tags: Record<string, any> = {}
|
|
123
|
+
const flagValues = this.defaults()
|
|
124
|
+
const currentTenantId = context.getTenantId()
|
|
125
|
+
const specificallySetFalse = new Set<string>()
|
|
126
|
+
|
|
127
|
+
const split = (env.TENANT_FEATURE_FLAGS || "")
|
|
128
|
+
.split(",")
|
|
129
|
+
.map(x => x.split(":"))
|
|
130
|
+
for (const [tenantId, ...features] of split) {
|
|
131
|
+
if (!tenantId || (tenantId !== "*" && tenantId !== currentTenantId)) {
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
tags[`readFromEnvironmentVars`] = true
|
|
136
|
+
|
|
137
|
+
for (let feature of features) {
|
|
138
|
+
let value = true
|
|
139
|
+
if (feature.startsWith("!")) {
|
|
140
|
+
feature = feature.slice(1)
|
|
141
|
+
value = false
|
|
142
|
+
specificallySetFalse.add(feature)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!this.isFlagName(feature)) {
|
|
146
|
+
throw new Error(`Feature: ${feature} is not an allowed option`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof flagValues[feature] !== "boolean") {
|
|
150
|
+
throw new Error(`Feature: ${feature} is not a boolean`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// @ts-expect-error - TS does not like you writing into a generic type,
|
|
154
|
+
// but we know that it's okay in this case because it's just an object.
|
|
155
|
+
flagValues[feature] = value
|
|
156
|
+
tags[`flags.${feature}.source`] = "environment"
|
|
157
|
+
}
|
|
76
158
|
}
|
|
77
159
|
|
|
78
|
-
|
|
79
|
-
|
|
160
|
+
const license = ctx?.user?.license
|
|
161
|
+
if (license) {
|
|
162
|
+
tags[`readFromLicense`] = true
|
|
163
|
+
|
|
164
|
+
for (const feature of license.features) {
|
|
165
|
+
if (!this.isFlagName(feature)) {
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
flagValues[feature] === true ||
|
|
171
|
+
specificallySetFalse.has(feature)
|
|
172
|
+
) {
|
|
173
|
+
// If the flag is already set to through environment variables, we
|
|
174
|
+
// don't want to override it back to false here.
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// @ts-expect-error - TS does not like you writing into a generic type,
|
|
179
|
+
// but we know that it's okay in this case because it's just an object.
|
|
180
|
+
flagValues[feature] = true
|
|
181
|
+
tags[`flags.${feature}.source`] = "license"
|
|
182
|
+
}
|
|
80
183
|
}
|
|
81
184
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
185
|
+
const identity = context.getIdentity()
|
|
186
|
+
tags[`identity.type`] = identity?.type
|
|
187
|
+
tags[`identity.tenantId`] = identity?.tenantId
|
|
188
|
+
tags[`identity._id`] = identity?._id
|
|
86
189
|
|
|
87
|
-
|
|
88
|
-
|
|
190
|
+
// Until we're confident this performs well, we're only enabling it in QA
|
|
191
|
+
// and test environments.
|
|
192
|
+
const usePosthog = env.isTest() || env.isQA()
|
|
193
|
+
if (usePosthog && posthog && identity?.type === IdentityType.USER) {
|
|
194
|
+
tags[`readFromPostHog`] = true
|
|
89
195
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
196
|
+
const personProperties: Record<string, string> = {}
|
|
197
|
+
if (identity.tenantId) {
|
|
198
|
+
personProperties.tenantId = identity.tenantId
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const posthogFlags = await posthog.getAllFlagsAndPayloads(
|
|
202
|
+
identity._id,
|
|
203
|
+
{
|
|
204
|
+
personProperties,
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
console.log("posthog flags", JSON.stringify(posthogFlags))
|
|
208
|
+
|
|
209
|
+
for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
|
|
210
|
+
if (!this.isFlagName(name)) {
|
|
211
|
+
// We don't want an unexpected PostHog flag to break the app, so we
|
|
212
|
+
// just log it and continue.
|
|
213
|
+
console.warn(`Unexpected posthog flag "${name}": ${value}`)
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
96
216
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
217
|
+
if (flagValues[name] === true || specificallySetFalse.has(name)) {
|
|
218
|
+
// If the flag is already set to through environment variables, we
|
|
219
|
+
// don't want to override it back to false here.
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const payload = posthogFlags.featureFlagPayloads?.[name]
|
|
224
|
+
const flag = this.flagSchema[name]
|
|
225
|
+
try {
|
|
226
|
+
// @ts-expect-error - TS does not like you writing into a generic
|
|
227
|
+
// type, but we know that it's okay in this case because it's just
|
|
228
|
+
// an object.
|
|
229
|
+
flagValues[name] = flag.parse(payload || value)
|
|
230
|
+
tags[`flags.${name}.source`] = "posthog"
|
|
231
|
+
} catch (err) {
|
|
232
|
+
// We don't want an invalid PostHog flag to break the app, so we just
|
|
233
|
+
// log it and continue.
|
|
234
|
+
console.warn(`Error parsing posthog flag "${name}": ${value}`, err)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const [key, value] of Object.entries(flagValues)) {
|
|
240
|
+
tags[`flags.${key}.value`] = value
|
|
241
|
+
}
|
|
242
|
+
span?.addTags(tags)
|
|
243
|
+
|
|
244
|
+
return flagValues
|
|
245
|
+
})
|
|
246
|
+
}
|
|
108
247
|
}
|
|
248
|
+
|
|
249
|
+
// This is the primary source of truth for feature flags. If you want to add a
|
|
250
|
+
// new flag, add it here and use the `fetch` and `get` functions to access it.
|
|
251
|
+
// All of the machinery in this file is to make sure that flags have their
|
|
252
|
+
// default values set correctly and their types flow through the system.
|
|
253
|
+
export const flags = new FlagSet({
|
|
254
|
+
LICENSING: Flag.boolean(false),
|
|
255
|
+
GOOGLE_SHEETS: Flag.boolean(false),
|
|
256
|
+
USER_GROUPS: Flag.boolean(false),
|
|
257
|
+
ONBOARDING_TOUR: Flag.boolean(false),
|
|
258
|
+
})
|