@budibase/backend-core 2.9.40-alpha.6 → 2.10.1

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.
Files changed (252) hide show
  1. package/dist/index.js +5 -4
  2. package/dist/index.js.map +2 -2
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +6 -6
  5. package/dist/src/cache/appMetadata.js +1 -1
  6. package/dist/src/cache/appMetadata.js.map +1 -1
  7. package/dist/src/constants/misc.d.ts +0 -2
  8. package/dist/src/constants/misc.js +0 -2
  9. package/dist/src/constants/misc.js.map +1 -1
  10. package/dist/src/environment.js +5 -4
  11. package/dist/src/environment.js.map +1 -1
  12. package/dist/src/logging/system.d.ts +1 -1
  13. package/dist/src/timers/timers.d.ts +1 -1
  14. package/package.json +6 -6
  15. package/src/accounts/accounts.ts +82 -0
  16. package/src/accounts/api.ts +59 -0
  17. package/src/accounts/index.ts +1 -0
  18. package/src/auth/auth.ts +208 -0
  19. package/src/auth/index.ts +1 -0
  20. package/src/auth/tests/auth.spec.ts +14 -0
  21. package/src/blacklist/blacklist.ts +54 -0
  22. package/src/blacklist/index.ts +1 -0
  23. package/src/blacklist/tests/blacklist.spec.ts +46 -0
  24. package/src/cache/appMetadata.ts +88 -0
  25. package/src/cache/base/index.ts +92 -0
  26. package/src/cache/generic.ts +30 -0
  27. package/src/cache/index.ts +5 -0
  28. package/src/cache/tests/writethrough.spec.ts +138 -0
  29. package/src/cache/user.ts +83 -0
  30. package/src/cache/writethrough.ts +133 -0
  31. package/src/configs/configs.ts +257 -0
  32. package/src/configs/index.ts +1 -0
  33. package/src/configs/tests/configs.spec.ts +184 -0
  34. package/src/constants/db.ts +63 -0
  35. package/src/constants/index.ts +2 -0
  36. package/src/constants/misc.ts +50 -0
  37. package/src/context/Context.ts +14 -0
  38. package/src/context/identity.ts +58 -0
  39. package/src/context/index.ts +3 -0
  40. package/src/context/mainContext.ts +310 -0
  41. package/src/context/tests/index.spec.ts +147 -0
  42. package/src/context/types.ts +11 -0
  43. package/src/db/Replication.ts +84 -0
  44. package/src/db/constants.ts +10 -0
  45. package/src/db/couch/DatabaseImpl.ts +238 -0
  46. package/src/db/couch/connections.ts +77 -0
  47. package/src/db/couch/index.ts +5 -0
  48. package/src/db/couch/pouchDB.ts +97 -0
  49. package/src/db/couch/pouchDump.ts +0 -0
  50. package/src/db/couch/utils.ts +50 -0
  51. package/src/db/db.ts +43 -0
  52. package/src/db/errors.ts +14 -0
  53. package/src/db/index.ts +12 -0
  54. package/src/db/lucene.ts +750 -0
  55. package/src/db/searchIndexes/index.ts +1 -0
  56. package/src/db/searchIndexes/searchIndexes.ts +62 -0
  57. package/src/db/tests/index.spec.js +25 -0
  58. package/src/db/tests/lucene.spec.ts +368 -0
  59. package/src/db/tests/pouch.spec.js +62 -0
  60. package/src/db/tests/utils.spec.ts +63 -0
  61. package/src/db/utils.ts +207 -0
  62. package/src/db/views.ts +241 -0
  63. package/src/docIds/conversions.ts +59 -0
  64. package/src/docIds/ids.ts +113 -0
  65. package/src/docIds/index.ts +2 -0
  66. package/src/docIds/newid.ts +5 -0
  67. package/src/docIds/params.ts +174 -0
  68. package/src/docUpdates/index.ts +29 -0
  69. package/src/environment.ts +201 -0
  70. package/src/errors/errors.ts +119 -0
  71. package/src/errors/index.ts +1 -0
  72. package/src/events/analytics.ts +6 -0
  73. package/src/events/asyncEvents/index.ts +2 -0
  74. package/src/events/asyncEvents/publisher.ts +12 -0
  75. package/src/events/asyncEvents/queue.ts +22 -0
  76. package/src/events/backfill.ts +183 -0
  77. package/src/events/documentId.ts +56 -0
  78. package/src/events/events.ts +40 -0
  79. package/src/events/identification.ts +310 -0
  80. package/src/events/index.ts +14 -0
  81. package/src/events/processors/AnalyticsProcessor.ts +64 -0
  82. package/src/events/processors/AuditLogsProcessor.ts +93 -0
  83. package/src/events/processors/LoggingProcessor.ts +37 -0
  84. package/src/events/processors/Processors.ts +52 -0
  85. package/src/events/processors/async/DocumentUpdateProcessor.ts +43 -0
  86. package/src/events/processors/index.ts +19 -0
  87. package/src/events/processors/posthog/PosthogProcessor.ts +118 -0
  88. package/src/events/processors/posthog/index.ts +2 -0
  89. package/src/events/processors/posthog/rateLimiting.ts +106 -0
  90. package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +168 -0
  91. package/src/events/processors/types.ts +1 -0
  92. package/src/events/publishers/account.ts +35 -0
  93. package/src/events/publishers/app.ts +155 -0
  94. package/src/events/publishers/auditLog.ts +26 -0
  95. package/src/events/publishers/auth.ts +73 -0
  96. package/src/events/publishers/automation.ts +110 -0
  97. package/src/events/publishers/backfill.ts +74 -0
  98. package/src/events/publishers/backup.ts +42 -0
  99. package/src/events/publishers/datasource.ts +48 -0
  100. package/src/events/publishers/email.ts +17 -0
  101. package/src/events/publishers/environmentVariable.ts +38 -0
  102. package/src/events/publishers/group.ts +99 -0
  103. package/src/events/publishers/index.ts +24 -0
  104. package/src/events/publishers/installation.ts +38 -0
  105. package/src/events/publishers/layout.ts +26 -0
  106. package/src/events/publishers/license.ts +84 -0
  107. package/src/events/publishers/org.ts +37 -0
  108. package/src/events/publishers/plugin.ts +47 -0
  109. package/src/events/publishers/query.ts +88 -0
  110. package/src/events/publishers/role.ts +62 -0
  111. package/src/events/publishers/rows.ts +29 -0
  112. package/src/events/publishers/screen.ts +36 -0
  113. package/src/events/publishers/serve.ts +43 -0
  114. package/src/events/publishers/table.ts +70 -0
  115. package/src/events/publishers/user.ts +202 -0
  116. package/src/events/publishers/view.ts +107 -0
  117. package/src/features/index.ts +78 -0
  118. package/src/features/installation.ts +17 -0
  119. package/src/features/tests/featureFlags.spec.ts +85 -0
  120. package/src/helpers.ts +9 -0
  121. package/src/index.ts +54 -0
  122. package/src/installation.ts +107 -0
  123. package/src/logging/alerts.ts +26 -0
  124. package/src/logging/correlation/correlation.ts +13 -0
  125. package/src/logging/correlation/index.ts +1 -0
  126. package/src/logging/correlation/middleware.ts +17 -0
  127. package/src/logging/index.ts +4 -0
  128. package/src/logging/pino/logger.ts +232 -0
  129. package/src/logging/pino/middleware.ts +45 -0
  130. package/src/logging/system.ts +81 -0
  131. package/src/logging/tests/system.spec.ts +61 -0
  132. package/src/middleware/adminOnly.ts +9 -0
  133. package/src/middleware/auditLog.ts +6 -0
  134. package/src/middleware/authenticated.ts +193 -0
  135. package/src/middleware/builderOnly.ts +21 -0
  136. package/src/middleware/builderOrAdmin.ts +21 -0
  137. package/src/middleware/csrf.ts +81 -0
  138. package/src/middleware/errorHandling.ts +29 -0
  139. package/src/middleware/index.ts +21 -0
  140. package/src/middleware/internalApi.ts +23 -0
  141. package/src/middleware/joi-validator.ts +45 -0
  142. package/src/middleware/matchers.ts +47 -0
  143. package/src/middleware/passport/datasource/google.ts +95 -0
  144. package/src/middleware/passport/local.ts +54 -0
  145. package/src/middleware/passport/sso/google.ts +77 -0
  146. package/src/middleware/passport/sso/oidc.ts +154 -0
  147. package/src/middleware/passport/sso/sso.ts +165 -0
  148. package/src/middleware/passport/sso/tests/google.spec.ts +67 -0
  149. package/src/middleware/passport/sso/tests/oidc.spec.ts +152 -0
  150. package/src/middleware/passport/sso/tests/sso.spec.ts +197 -0
  151. package/src/middleware/passport/utils.ts +38 -0
  152. package/src/middleware/querystringToBody.ts +28 -0
  153. package/src/middleware/tenancy.ts +36 -0
  154. package/src/middleware/tests/builder.spec.ts +180 -0
  155. package/src/middleware/tests/matchers.spec.ts +134 -0
  156. package/src/migrations/definitions.ts +40 -0
  157. package/src/migrations/index.ts +2 -0
  158. package/src/migrations/migrations.ts +191 -0
  159. package/src/migrations/tests/__snapshots__/migrations.spec.ts.snap +11 -0
  160. package/src/migrations/tests/migrations.spec.ts +64 -0
  161. package/src/objectStore/buckets/app.ts +40 -0
  162. package/src/objectStore/buckets/global.ts +29 -0
  163. package/src/objectStore/buckets/index.ts +3 -0
  164. package/src/objectStore/buckets/plugins.ts +71 -0
  165. package/src/objectStore/buckets/tests/app.spec.ts +171 -0
  166. package/src/objectStore/buckets/tests/global.spec.ts +74 -0
  167. package/src/objectStore/buckets/tests/plugins.spec.ts +111 -0
  168. package/src/objectStore/cloudfront.ts +41 -0
  169. package/src/objectStore/index.ts +3 -0
  170. package/src/objectStore/objectStore.ts +440 -0
  171. package/src/objectStore/utils.ts +27 -0
  172. package/src/platform/index.ts +3 -0
  173. package/src/platform/platformDb.ts +6 -0
  174. package/src/platform/tenants.ts +101 -0
  175. package/src/platform/tests/tenants.spec.ts +26 -0
  176. package/src/platform/users.ts +90 -0
  177. package/src/plugin/index.ts +1 -0
  178. package/src/plugin/tests/validation.spec.ts +83 -0
  179. package/src/plugin/utils.ts +156 -0
  180. package/src/queue/constants.ts +6 -0
  181. package/src/queue/inMemoryQueue.ts +141 -0
  182. package/src/queue/index.ts +2 -0
  183. package/src/queue/listeners.ts +195 -0
  184. package/src/queue/queue.ts +54 -0
  185. package/src/redis/index.ts +6 -0
  186. package/src/redis/init.ts +86 -0
  187. package/src/redis/redis.ts +308 -0
  188. package/src/redis/redlockImpl.ts +139 -0
  189. package/src/redis/utils.ts +117 -0
  190. package/src/security/encryption.ts +179 -0
  191. package/src/security/permissions.ts +158 -0
  192. package/src/security/roles.ts +389 -0
  193. package/src/security/sessions.ts +120 -0
  194. package/src/security/tests/encryption.spec.ts +31 -0
  195. package/src/security/tests/permissions.spec.ts +145 -0
  196. package/src/security/tests/sessions.spec.ts +12 -0
  197. package/src/tenancy/db.ts +6 -0
  198. package/src/tenancy/index.ts +2 -0
  199. package/src/tenancy/tenancy.ts +140 -0
  200. package/src/tenancy/tests/tenancy.spec.ts +184 -0
  201. package/src/timers/index.ts +1 -0
  202. package/src/timers/timers.ts +22 -0
  203. package/src/users/db.ts +484 -0
  204. package/src/users/events.ts +176 -0
  205. package/src/users/index.ts +4 -0
  206. package/src/users/lookup.ts +102 -0
  207. package/src/users/users.ts +276 -0
  208. package/src/users/utils.ts +55 -0
  209. package/src/utils/hashing.ts +14 -0
  210. package/src/utils/index.ts +3 -0
  211. package/src/utils/stringUtils.ts +8 -0
  212. package/src/utils/tests/utils.spec.ts +191 -0
  213. package/src/utils/utils.ts +239 -0
  214. package/tests/core/logging.ts +34 -0
  215. package/tests/core/utilities/index.ts +6 -0
  216. package/tests/core/utilities/jestUtils.ts +30 -0
  217. package/tests/core/utilities/mocks/alerts.ts +3 -0
  218. package/tests/core/utilities/mocks/date.ts +2 -0
  219. package/tests/core/utilities/mocks/events.ts +131 -0
  220. package/tests/core/utilities/mocks/fetch.ts +17 -0
  221. package/tests/core/utilities/mocks/index.ts +10 -0
  222. package/tests/core/utilities/mocks/licenses.ts +115 -0
  223. package/tests/core/utilities/mocks/posthog.ts +7 -0
  224. package/tests/core/utilities/structures/Chance.ts +20 -0
  225. package/tests/core/utilities/structures/accounts.ts +115 -0
  226. package/tests/core/utilities/structures/apps.ts +21 -0
  227. package/tests/core/utilities/structures/common.ts +7 -0
  228. package/tests/core/utilities/structures/db.ts +12 -0
  229. package/tests/core/utilities/structures/documents/index.ts +1 -0
  230. package/tests/core/utilities/structures/documents/platform/index.ts +1 -0
  231. package/tests/core/utilities/structures/documents/platform/installation.ts +12 -0
  232. package/tests/core/utilities/structures/generator.ts +2 -0
  233. package/tests/core/utilities/structures/index.ts +15 -0
  234. package/tests/core/utilities/structures/koa.ts +16 -0
  235. package/tests/core/utilities/structures/licenses.ts +167 -0
  236. package/tests/core/utilities/structures/plugins.ts +19 -0
  237. package/tests/core/utilities/structures/quotas.ts +67 -0
  238. package/tests/core/utilities/structures/scim.ts +80 -0
  239. package/tests/core/utilities/structures/shared.ts +19 -0
  240. package/tests/core/utilities/structures/sso.ts +119 -0
  241. package/tests/core/utilities/structures/tenants.ts +5 -0
  242. package/tests/core/utilities/structures/userGroups.ts +10 -0
  243. package/tests/core/utilities/structures/users.ts +73 -0
  244. package/tests/core/utilities/testContainerUtils.ts +85 -0
  245. package/tests/core/utilities/utils/index.ts +1 -0
  246. package/tests/core/utilities/utils/time.ts +3 -0
  247. package/tests/extra/DBTestConfiguration.ts +36 -0
  248. package/tests/extra/index.ts +2 -0
  249. package/tests/extra/testEnv.ts +95 -0
  250. package/tests/index.ts +1 -0
  251. package/tests/jestEnv.ts +6 -0
  252. package/tests/jestSetup.ts +28 -0
@@ -0,0 +1,93 @@
1
+ import {
2
+ Event,
3
+ Identity,
4
+ Group,
5
+ IdentityType,
6
+ AuditLogQueueEvent,
7
+ AuditLogFn,
8
+ HostInfo,
9
+ } from "@budibase/types"
10
+ import { EventProcessor } from "./types"
11
+ import { getAppId, doInTenant, getTenantId } from "../../context"
12
+ import BullQueue from "bull"
13
+ import { createQueue, JobQueue } from "../../queue"
14
+ import { isAudited } from "../../utils"
15
+ import env from "../../environment"
16
+
17
+ export default class AuditLogsProcessor implements EventProcessor {
18
+ static auditLogsEnabled = false
19
+ static auditLogQueue: BullQueue.Queue<AuditLogQueueEvent>
20
+
21
+ // can't use constructor as need to return promise
22
+ static init(fn: AuditLogFn) {
23
+ AuditLogsProcessor.auditLogsEnabled = true
24
+ const writeAuditLogs = fn
25
+ AuditLogsProcessor.auditLogQueue = createQueue<AuditLogQueueEvent>(
26
+ JobQueue.AUDIT_LOG
27
+ )
28
+ return AuditLogsProcessor.auditLogQueue.process(async job => {
29
+ return doInTenant(job.data.tenantId, async () => {
30
+ let properties = job.data.properties
31
+ if (properties.audited) {
32
+ properties = {
33
+ ...properties,
34
+ ...properties.audited,
35
+ }
36
+ delete properties.audited
37
+ }
38
+
39
+ // this feature is disabled by default due to privacy requirements
40
+ // in some countries - available as env var in-case it is desired
41
+ // in self host deployments
42
+ let hostInfo: HostInfo | undefined = {}
43
+ if (env.ENABLE_AUDIT_LOG_IP_ADDR) {
44
+ hostInfo = job.data.opts.hostInfo
45
+ }
46
+
47
+ await writeAuditLogs(job.data.event, properties, {
48
+ userId: job.data.opts.userId,
49
+ timestamp: job.data.opts.timestamp,
50
+ appId: job.data.opts.appId,
51
+ hostInfo,
52
+ })
53
+ })
54
+ })
55
+ }
56
+
57
+ async processEvent(
58
+ event: Event,
59
+ identity: Identity,
60
+ properties: any,
61
+ timestamp?: string
62
+ ): Promise<void> {
63
+ if (AuditLogsProcessor.auditLogsEnabled && isAudited(event)) {
64
+ // only audit log actual events, don't include backfills
65
+ const userId =
66
+ identity.type === IdentityType.USER ? identity.id : undefined
67
+ // add to the event queue, rather than just writing immediately
68
+ await AuditLogsProcessor.auditLogQueue.add({
69
+ event,
70
+ properties,
71
+ opts: {
72
+ userId,
73
+ timestamp,
74
+ appId: getAppId(),
75
+ hostInfo: identity.hostInfo,
76
+ },
77
+ tenantId: getTenantId(),
78
+ })
79
+ }
80
+ }
81
+
82
+ async identify(identity: Identity, timestamp?: string | number) {
83
+ // no-op
84
+ }
85
+
86
+ async identifyGroup(group: Group, timestamp?: string | number) {
87
+ // no-op
88
+ }
89
+
90
+ shutdown(): void {
91
+ AuditLogsProcessor.auditLogQueue?.close()
92
+ }
93
+ }
@@ -0,0 +1,37 @@
1
+ import { Event, Identity, Group } from "@budibase/types"
2
+ import { EventProcessor } from "./types"
3
+ import env from "../../environment"
4
+
5
+ const skipLogging = env.SELF_HOSTED && !env.isDev()
6
+
7
+ export default class LoggingProcessor implements EventProcessor {
8
+ async processEvent(
9
+ event: Event,
10
+ identity: Identity,
11
+ properties: any,
12
+ timestamp?: string
13
+ ): Promise<void> {
14
+ if (skipLogging) {
15
+ return
16
+ }
17
+ console.log(`[audit] [identityType=${identity.type}] ${event}`, properties)
18
+ }
19
+
20
+ async identify(identity: Identity, timestamp?: string | number) {
21
+ if (skipLogging) {
22
+ return
23
+ }
24
+ console.log(`[audit] identified`, identity)
25
+ }
26
+
27
+ async identifyGroup(group: Group, timestamp?: string | number) {
28
+ if (skipLogging) {
29
+ return
30
+ }
31
+ console.log(`[audit] group identified`, group)
32
+ }
33
+
34
+ shutdown(): void {
35
+ // no-op
36
+ }
37
+ }
@@ -0,0 +1,52 @@
1
+ import { Event, Identity, Group } from "@budibase/types"
2
+ import { EventProcessor } from "./types"
3
+
4
+ export default class Processor implements EventProcessor {
5
+ initialised: boolean = false
6
+ processors: EventProcessor[] = []
7
+
8
+ constructor(processors: EventProcessor[]) {
9
+ this.processors = processors
10
+ }
11
+
12
+ async processEvent(
13
+ event: Event,
14
+ identity: Identity,
15
+ properties: any,
16
+ timestamp?: string | number
17
+ ): Promise<void> {
18
+ for (const eventProcessor of this.processors) {
19
+ await eventProcessor.processEvent(event, identity, properties, timestamp)
20
+ }
21
+ }
22
+
23
+ async identify(
24
+ identity: Identity,
25
+ timestamp?: string | number
26
+ ): Promise<void> {
27
+ for (const eventProcessor of this.processors) {
28
+ if (eventProcessor.identify) {
29
+ await eventProcessor.identify(identity, timestamp)
30
+ }
31
+ }
32
+ }
33
+
34
+ async identifyGroup(
35
+ identity: Group,
36
+ timestamp?: string | number
37
+ ): Promise<void> {
38
+ for (const eventProcessor of this.processors) {
39
+ if (eventProcessor.identifyGroup) {
40
+ await eventProcessor.identifyGroup(identity, timestamp)
41
+ }
42
+ }
43
+ }
44
+
45
+ shutdown() {
46
+ for (const eventProcessor of this.processors) {
47
+ if (eventProcessor.shutdown) {
48
+ eventProcessor.shutdown()
49
+ }
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,43 @@
1
+ import { EventProcessor } from "../types"
2
+ import { Event, Identity, DocUpdateEvent } from "@budibase/types"
3
+ import { doInTenant } from "../../../context"
4
+ import { getDocumentId } from "../../documentId"
5
+ import { shutdown } from "../../asyncEvents"
6
+
7
+ export type Processor = (update: DocUpdateEvent) => Promise<void>
8
+ export type ProcessorMap = { events: Event[]; processor: Processor }[]
9
+
10
+ export default class DocumentUpdateProcessor implements EventProcessor {
11
+ processors: ProcessorMap = []
12
+
13
+ constructor(processors: ProcessorMap) {
14
+ this.processors = processors
15
+ }
16
+
17
+ async processEvent(
18
+ event: Event,
19
+ identity: Identity,
20
+ properties: any,
21
+ timestamp?: string | number
22
+ ) {
23
+ const tenantId = identity.realTenantId
24
+ const docId = getDocumentId(event, properties)
25
+ if (!tenantId || !docId) {
26
+ return
27
+ }
28
+ for (let { events, processor } of this.processors) {
29
+ if (events.includes(event)) {
30
+ await doInTenant(tenantId, async () => {
31
+ await processor({
32
+ id: docId,
33
+ tenantId,
34
+ })
35
+ })
36
+ }
37
+ }
38
+ }
39
+
40
+ shutdown() {
41
+ return shutdown()
42
+ }
43
+ }
@@ -0,0 +1,19 @@
1
+ import AnalyticsProcessor from "./AnalyticsProcessor"
2
+ import LoggingProcessor from "./LoggingProcessor"
3
+ import AuditLogsProcessor from "./AuditLogsProcessor"
4
+ import Processors from "./Processors"
5
+ import { AuditLogFn } from "@budibase/types"
6
+
7
+ export const analyticsProcessor = new AnalyticsProcessor()
8
+ const loggingProcessor = new LoggingProcessor()
9
+ const auditLogsProcessor = new AuditLogsProcessor()
10
+
11
+ export function init(auditingFn: AuditLogFn) {
12
+ return AuditLogsProcessor.init(auditingFn)
13
+ }
14
+
15
+ export const processors = new Processors([
16
+ analyticsProcessor,
17
+ loggingProcessor,
18
+ auditLogsProcessor,
19
+ ])
@@ -0,0 +1,118 @@
1
+ import PostHog from "posthog-node"
2
+ import { Event, Identity, Group, BaseEvent } from "@budibase/types"
3
+ import { EventProcessor } from "../types"
4
+ import env from "../../../environment"
5
+ import * as context from "../../../context"
6
+ import * as rateLimiting from "./rateLimiting"
7
+
8
+ const EXCLUDED_EVENTS: Event[] = [
9
+ Event.USER_UPDATED,
10
+ Event.EMAIL_SMTP_UPDATED,
11
+ Event.AUTH_SSO_UPDATED,
12
+ Event.APP_UPDATED,
13
+ Event.ROLE_UPDATED,
14
+ Event.DATASOURCE_UPDATED,
15
+ Event.QUERY_UPDATED,
16
+ Event.TABLE_UPDATED,
17
+ Event.VIEW_UPDATED,
18
+ Event.VIEW_FILTER_UPDATED,
19
+ Event.VIEW_CALCULATION_UPDATED,
20
+ Event.AUTOMATION_TRIGGER_UPDATED,
21
+ Event.USER_GROUP_UPDATED,
22
+ ]
23
+
24
+ export default class PosthogProcessor implements EventProcessor {
25
+ posthog: PostHog
26
+
27
+ constructor(token: string | undefined) {
28
+ if (!token) {
29
+ throw new Error("Posthog token is not defined")
30
+ }
31
+ this.posthog = new PostHog(token)
32
+ }
33
+
34
+ async processEvent(
35
+ event: Event,
36
+ identity: Identity,
37
+ properties: BaseEvent,
38
+ timestamp?: string | number
39
+ ): Promise<void> {
40
+ // don't send excluded events
41
+ if (EXCLUDED_EVENTS.includes(event)) {
42
+ return
43
+ }
44
+
45
+ if (await rateLimiting.limited(event)) {
46
+ return
47
+ }
48
+
49
+ properties = this.clearPIIProperties(properties)
50
+
51
+ properties.version = env.VERSION
52
+ properties.service = env.SERVICE
53
+ properties.environment = identity.environment
54
+ properties.hosting = identity.hosting
55
+
56
+ const appId = context.getAppId()
57
+ if (appId) {
58
+ properties.appId = appId
59
+ }
60
+
61
+ const payload: any = { distinctId: identity.id, event, properties }
62
+
63
+ if (timestamp) {
64
+ payload.timestamp = new Date(timestamp)
65
+ }
66
+
67
+ // add groups to the event
68
+ if (identity.installationId || identity.tenantId) {
69
+ payload.groups = {}
70
+ if (identity.installationId) {
71
+ payload.groups.installation = identity.installationId
72
+ payload.properties.installationId = identity.installationId
73
+ }
74
+ if (identity.tenantId) {
75
+ payload.groups.tenant = identity.tenantId
76
+ payload.properties.tenantId = identity.tenantId
77
+ }
78
+ }
79
+
80
+ this.posthog.capture(payload)
81
+ }
82
+
83
+ clearPIIProperties(properties: any) {
84
+ if (properties.email) {
85
+ delete properties.email
86
+ }
87
+ if (properties.audited) {
88
+ delete properties.audited
89
+ }
90
+ return properties
91
+ }
92
+
93
+ async identify(identity: Identity, timestamp?: string | number) {
94
+ const payload: any = { distinctId: identity.id, properties: identity }
95
+ if (timestamp) {
96
+ payload.timestamp = new Date(timestamp)
97
+ }
98
+ this.posthog.identify(payload)
99
+ }
100
+
101
+ async identifyGroup(group: Group, timestamp?: string | number) {
102
+ const payload: any = {
103
+ distinctId: group.id,
104
+ groupType: group.type,
105
+ groupKey: group.id,
106
+ properties: group,
107
+ }
108
+
109
+ if (timestamp) {
110
+ payload.timestamp = new Date(timestamp)
111
+ }
112
+ this.posthog.groupIdentify(payload)
113
+ }
114
+
115
+ shutdown() {
116
+ this.posthog.shutdown()
117
+ }
118
+ }
@@ -0,0 +1,2 @@
1
+ import PosthogProcessor from "./PosthogProcessor"
2
+ export default PosthogProcessor
@@ -0,0 +1,106 @@
1
+ import { Event } from "@budibase/types"
2
+ import { CacheKey, TTL } from "../../../cache/generic"
3
+ import * as cache from "../../../cache/generic"
4
+ import * as context from "../../../context"
5
+
6
+ type RateLimitedEvent =
7
+ | Event.SERVED_BUILDER
8
+ | Event.SERVED_APP_PREVIEW
9
+ | Event.SERVED_APP
10
+
11
+ const isRateLimited = (event: Event): event is RateLimitedEvent => {
12
+ return (
13
+ event === Event.SERVED_BUILDER ||
14
+ event === Event.SERVED_APP_PREVIEW ||
15
+ event === Event.SERVED_APP
16
+ )
17
+ }
18
+
19
+ const isPerApp = (event: RateLimitedEvent) => {
20
+ return event === Event.SERVED_APP_PREVIEW || event === Event.SERVED_APP
21
+ }
22
+
23
+ interface EventProperties {
24
+ timestamp: number
25
+ }
26
+
27
+ enum RateLimit {
28
+ CALENDAR_DAY = "calendarDay",
29
+ }
30
+
31
+ const RATE_LIMITS = {
32
+ [Event.SERVED_APP]: RateLimit.CALENDAR_DAY,
33
+ [Event.SERVED_APP_PREVIEW]: RateLimit.CALENDAR_DAY,
34
+ [Event.SERVED_BUILDER]: RateLimit.CALENDAR_DAY,
35
+ }
36
+
37
+ /**
38
+ * Check if this event should be sent right now
39
+ * Return false to signal the event SHOULD be sent
40
+ * Return true to signal the event should NOT be sent
41
+ */
42
+ export const limited = async (event: Event): Promise<boolean> => {
43
+ // not a rate limited event -- send
44
+ if (!isRateLimited(event)) {
45
+ return false
46
+ }
47
+
48
+ const cachedEvent = await readEvent(event)
49
+ if (cachedEvent) {
50
+ const timestamp = new Date(cachedEvent.timestamp)
51
+ const limit = RATE_LIMITS[event]
52
+ switch (limit) {
53
+ case RateLimit.CALENDAR_DAY: {
54
+ // get midnight at the start of the next day for the timestamp
55
+ timestamp.setDate(timestamp.getDate() + 1)
56
+ timestamp.setHours(0, 0, 0, 0)
57
+
58
+ // if we have passed the threshold into the next day
59
+ if (Date.now() > timestamp.getTime()) {
60
+ // update the timestamp in the event -- send
61
+ await recordEvent(event, { timestamp: Date.now() })
62
+ return false
63
+ } else {
64
+ // still within the limited period -- don't send
65
+ return true
66
+ }
67
+ }
68
+ }
69
+ } else {
70
+ // no event present i.e. expired -- send
71
+ await recordEvent(event, { timestamp: Date.now() })
72
+ return false
73
+ }
74
+ }
75
+
76
+ const eventKey = (event: RateLimitedEvent) => {
77
+ let key = `${CacheKey.EVENTS_RATE_LIMIT}:${event}`
78
+ if (isPerApp(event)) {
79
+ key = key + ":" + context.getAppId()
80
+ }
81
+ return key
82
+ }
83
+
84
+ const readEvent = async (
85
+ event: RateLimitedEvent
86
+ ): Promise<EventProperties | undefined> => {
87
+ const key = eventKey(event)
88
+ const result = await cache.get(key)
89
+ return result as EventProperties
90
+ }
91
+
92
+ const recordEvent = async (
93
+ event: RateLimitedEvent,
94
+ properties: EventProperties
95
+ ) => {
96
+ const key = eventKey(event)
97
+ const limit = RATE_LIMITS[event]
98
+ let ttl
99
+ switch (limit) {
100
+ case RateLimit.CALENDAR_DAY: {
101
+ ttl = TTL.ONE_DAY
102
+ }
103
+ }
104
+
105
+ await cache.store(key, properties, ttl)
106
+ }
@@ -0,0 +1,168 @@
1
+ import { testEnv } from "../../../../../tests/extra"
2
+ import PosthogProcessor from "../PosthogProcessor"
3
+ import { Event, IdentityType, Hosting } from "@budibase/types"
4
+ const tk = require("timekeeper")
5
+ import * as cache from "../../../../cache/generic"
6
+ import { CacheKey } from "../../../../cache/generic"
7
+ import * as context from "../../../../context"
8
+
9
+ const newIdentity = () => {
10
+ return {
11
+ id: "test",
12
+ type: IdentityType.USER,
13
+ hosting: Hosting.SELF,
14
+ environment: "test",
15
+ }
16
+ }
17
+
18
+ describe("PosthogProcessor", () => {
19
+ beforeAll(() => {
20
+ testEnv.singleTenant()
21
+ })
22
+
23
+ beforeEach(async () => {
24
+ jest.clearAllMocks()
25
+ await cache.bustCache(
26
+ `${CacheKey.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}`
27
+ )
28
+ })
29
+
30
+ describe("processEvent", () => {
31
+ it("processes event", async () => {
32
+ const processor = new PosthogProcessor("test")
33
+
34
+ const identity = newIdentity()
35
+ const properties = {}
36
+
37
+ await processor.processEvent(Event.APP_CREATED, identity, properties)
38
+
39
+ expect(processor.posthog.capture).toHaveBeenCalledTimes(1)
40
+ })
41
+
42
+ it("honours exclusions", async () => {
43
+ const processor = new PosthogProcessor("test")
44
+
45
+ const identity = newIdentity()
46
+ const properties = {}
47
+
48
+ await processor.processEvent(Event.AUTH_SSO_UPDATED, identity, properties)
49
+ expect(processor.posthog.capture).toHaveBeenCalledTimes(0)
50
+ })
51
+
52
+ it("removes audited information", async () => {
53
+ const processor = new PosthogProcessor("test")
54
+
55
+ const identity = newIdentity()
56
+ const properties = {
57
+ email: "test",
58
+ audited: {
59
+ name: "test",
60
+ },
61
+ }
62
+
63
+ await processor.processEvent(Event.USER_CREATED, identity, properties)
64
+ expect(processor.posthog.capture).toHaveBeenCalled()
65
+ // @ts-ignore
66
+ const call = processor.posthog.capture.mock.calls[0][0]
67
+ expect(call.properties.audited).toBeUndefined()
68
+ expect(call.properties.email).toBeUndefined()
69
+ })
70
+
71
+ describe("rate limiting", () => {
72
+ it("sends daily event once in same day", async () => {
73
+ const processor = new PosthogProcessor("test")
74
+ const identity = newIdentity()
75
+ const properties = {}
76
+
77
+ tk.freeze(new Date(2022, 0, 1, 14, 0))
78
+ await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
79
+ // go forward one hour
80
+ tk.freeze(new Date(2022, 0, 1, 15, 0))
81
+ await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
82
+
83
+ expect(processor.posthog.capture).toHaveBeenCalledTimes(1)
84
+ })
85
+
86
+ it("sends daily event once per unique day", async () => {
87
+ const processor = new PosthogProcessor("test")
88
+ const identity = newIdentity()
89
+ const properties = {}
90
+
91
+ tk.freeze(new Date(2022, 0, 1, 14, 0))
92
+ await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
93
+ // go forward into next day
94
+ tk.freeze(new Date(2022, 0, 2, 9, 0))
95
+ await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
96
+ // go forward into next day
97
+ tk.freeze(new Date(2022, 0, 3, 5, 0))
98
+ await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
99
+ // go forward one hour
100
+ tk.freeze(new Date(2022, 0, 3, 6, 0))
101
+ await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
102
+
103
+ expect(processor.posthog.capture).toHaveBeenCalledTimes(3)
104
+ })
105
+
106
+ it("sends event again after cache expires", async () => {
107
+ const processor = new PosthogProcessor("test")
108
+ const identity = newIdentity()
109
+ const properties = {}
110
+
111
+ tk.freeze(new Date(2022, 0, 1, 14, 0))
112
+ await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
113
+
114
+ await cache.bustCache(
115
+ `${CacheKey.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}`
116
+ )
117
+
118
+ tk.freeze(new Date(2022, 0, 1, 14, 0))
119
+ await processor.processEvent(Event.SERVED_BUILDER, identity, properties)
120
+
121
+ expect(processor.posthog.capture).toHaveBeenCalledTimes(2)
122
+ })
123
+
124
+ it("sends per app events once per day per app", async () => {
125
+ const processor = new PosthogProcessor("test")
126
+ const identity = newIdentity()
127
+ const properties = {}
128
+
129
+ const runAppEvents = async (appId: string) => {
130
+ await context.doInAppContext(appId, async () => {
131
+ tk.freeze(new Date(2022, 0, 1, 14, 0))
132
+ await processor.processEvent(Event.SERVED_APP, identity, properties)
133
+ await processor.processEvent(
134
+ Event.SERVED_APP_PREVIEW,
135
+ identity,
136
+ properties
137
+ )
138
+
139
+ // go forward one hour - should be ignored
140
+ tk.freeze(new Date(2022, 0, 1, 15, 0))
141
+ await processor.processEvent(Event.SERVED_APP, identity, properties)
142
+ await processor.processEvent(
143
+ Event.SERVED_APP_PREVIEW,
144
+ identity,
145
+ properties
146
+ )
147
+
148
+ // go forward into next day
149
+ tk.freeze(new Date(2022, 0, 2, 9, 0))
150
+
151
+ await processor.processEvent(Event.SERVED_APP, identity, properties)
152
+ await processor.processEvent(
153
+ Event.SERVED_APP_PREVIEW,
154
+ identity,
155
+ properties
156
+ )
157
+ })
158
+ }
159
+
160
+ await runAppEvents("app_1")
161
+ expect(processor.posthog.capture).toHaveBeenCalledTimes(4)
162
+
163
+ await runAppEvents("app_2")
164
+ expect(processor.posthog.capture).toHaveBeenCalledTimes(8)
165
+ })
166
+ })
167
+ })
168
+ })
@@ -0,0 +1 @@
1
+ export { EventProcessor } from "@budibase/types"
@@ -0,0 +1,35 @@
1
+ import { publishEvent } from "../events"
2
+ import {
3
+ Event,
4
+ Account,
5
+ AccountCreatedEvent,
6
+ AccountDeletedEvent,
7
+ AccountVerifiedEvent,
8
+ } from "@budibase/types"
9
+
10
+ async function created(account: Account) {
11
+ const properties: AccountCreatedEvent = {
12
+ tenantId: account.tenantId,
13
+ }
14
+ await publishEvent(Event.ACCOUNT_CREATED, properties)
15
+ }
16
+
17
+ async function deleted(account: Account) {
18
+ const properties: AccountDeletedEvent = {
19
+ tenantId: account.tenantId,
20
+ }
21
+ await publishEvent(Event.ACCOUNT_DELETED, properties)
22
+ }
23
+
24
+ async function verified(account: Account) {
25
+ const properties: AccountVerifiedEvent = {
26
+ tenantId: account.tenantId,
27
+ }
28
+ await publishEvent(Event.ACCOUNT_VERIFIED, properties)
29
+ }
30
+
31
+ export default {
32
+ created,
33
+ deleted,
34
+ verified,
35
+ }