mirror of https://github.com/Budibase/budibase.git
7 changed files with 204 additions and 45 deletions
@ -0,0 +1,2 @@ |
|||||
|
import PosthogProcessor from "./PosthogProcessor" |
||||
|
export default PosthogProcessor |
||||
@ -0,0 +1,95 @@ |
|||||
|
import { Event } from "@budibase/types" |
||||
|
import { CacheKeys, TTL } from "../../../cache/generic" |
||||
|
import * as cache from "../../../cache/generic" |
||||
|
|
||||
|
type RateLimitedEvent = |
||||
|
| Event.SERVED_BUILDER |
||||
|
| Event.SERVED_APP_PREVIEW |
||||
|
| Event.SERVED_APP |
||||
|
|
||||
|
const isRateLimited = (event: Event): event is RateLimitedEvent => { |
||||
|
return ( |
||||
|
event === Event.SERVED_BUILDER || |
||||
|
event === Event.SERVED_APP_PREVIEW || |
||||
|
event === Event.SERVED_APP |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
interface EventProperties { |
||||
|
timestamp: number |
||||
|
} |
||||
|
|
||||
|
enum RateLimit { |
||||
|
CALENDAR_DAY = "calendarDay", |
||||
|
} |
||||
|
|
||||
|
const RATE_LIMITS = { |
||||
|
[Event.SERVED_APP]: RateLimit.CALENDAR_DAY, |
||||
|
[Event.SERVED_APP_PREVIEW]: RateLimit.CALENDAR_DAY, |
||||
|
[Event.SERVED_BUILDER]: RateLimit.CALENDAR_DAY, |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if this event should be sent right now |
||||
|
* Return false to signal the event SHOULD be sent |
||||
|
* Return true to signal the event should NOT be sent |
||||
|
*/ |
||||
|
export const limited = async (event: Event): Promise<boolean> => { |
||||
|
// not a rate limited event -- send
|
||||
|
if (!isRateLimited(event)) { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
const cachedEvent = (await readEvent(event)) as EventProperties |
||||
|
if (cachedEvent) { |
||||
|
const timestamp = new Date(cachedEvent.timestamp) |
||||
|
const limit = RATE_LIMITS[event] |
||||
|
switch (limit) { |
||||
|
case RateLimit.CALENDAR_DAY: { |
||||
|
// get midnight at the start of the next day for the timestamp
|
||||
|
timestamp.setDate(timestamp.getDate() + 1) |
||||
|
timestamp.setHours(0, 0, 0, 0) |
||||
|
|
||||
|
// if we have passed the threshold into the next day
|
||||
|
if (Date.now() > timestamp.getTime()) { |
||||
|
// update the timestamp in the event -- send
|
||||
|
await recordEvent(event, { timestamp: Date.now() }) |
||||
|
return false |
||||
|
} else { |
||||
|
// still within the limited period -- don't send
|
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
// no event present i.e. expired -- send
|
||||
|
await recordEvent(event, { timestamp: Date.now() }) |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const eventKey = (event: RateLimitedEvent) => { |
||||
|
return `${CacheKeys.EVENTS_RATE_LIMIT}:${event}` |
||||
|
} |
||||
|
|
||||
|
const readEvent = async (event: RateLimitedEvent) => { |
||||
|
const key = eventKey(event) |
||||
|
return cache.get(key) |
||||
|
} |
||||
|
|
||||
|
const recordEvent = async ( |
||||
|
event: RateLimitedEvent, |
||||
|
properties: EventProperties |
||||
|
) => { |
||||
|
const key = `${CacheKeys.EVENTS_RATE_LIMIT}:${event}` |
||||
|
|
||||
|
const limit = RATE_LIMITS[event] |
||||
|
let ttl |
||||
|
switch (limit) { |
||||
|
case RateLimit.CALENDAR_DAY: { |
||||
|
ttl = TTL.ONE_DAY |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await cache.store(key, properties, ttl) |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
import PosthogProcessor from "../PosthogProcessor" |
||||
|
import { Event, IdentityType, Hosting } from "@budibase/types" |
||||
|
const tk = require("timekeeper") |
||||
|
import * as Cache from "../../../../cache/generic" |
||||
|
|
||||
|
const newIdentity = () => { |
||||
|
return { |
||||
|
id: "test", |
||||
|
type: IdentityType.USER, |
||||
|
hosting: Hosting.SELF, |
||||
|
environment: "test", |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
describe("PosthogProcessor", () => { |
||||
|
beforeEach(() => { |
||||
|
jest.clearAllMocks() |
||||
|
}) |
||||
|
|
||||
|
describe("processEvent", () => { |
||||
|
it("processes event", async () => { |
||||
|
const processor = new PosthogProcessor("test") |
||||
|
|
||||
|
const identity = newIdentity() |
||||
|
const properties = {} |
||||
|
|
||||
|
await processor.processEvent(Event.APP_CREATED, identity, properties) |
||||
|
|
||||
|
expect(processor.posthog.capture).toHaveBeenCalledTimes(1) |
||||
|
}) |
||||
|
|
||||
|
it("honours exclusions", async () => { |
||||
|
const processor = new PosthogProcessor("test") |
||||
|
|
||||
|
const identity = newIdentity() |
||||
|
const properties = {} |
||||
|
|
||||
|
await processor.processEvent(Event.AUTH_SSO_UPDATED, identity, properties) |
||||
|
expect(processor.posthog.capture).toHaveBeenCalledTimes(0) |
||||
|
}) |
||||
|
|
||||
|
describe("rate limiting", () => { |
||||
|
it("sends daily event once in same day", async () => { |
||||
|
const processor = new PosthogProcessor("test") |
||||
|
const identity = newIdentity() |
||||
|
const properties = {} |
||||
|
|
||||
|
tk.freeze(new Date(2022, 0, 1, 14, 0)) |
||||
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties) |
||||
|
// go forward one hour
|
||||
|
tk.freeze(new Date(2022, 0, 1, 15, 0)) |
||||
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties) |
||||
|
|
||||
|
expect(processor.posthog.capture).toHaveBeenCalledTimes(1) |
||||
|
}) |
||||
|
|
||||
|
it("sends daily event once per unique day", async () => { |
||||
|
const processor = new PosthogProcessor("test") |
||||
|
const identity = newIdentity() |
||||
|
const properties = {} |
||||
|
|
||||
|
tk.freeze(new Date(2022, 0, 1, 14, 0)) |
||||
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties) |
||||
|
// go forward into next day
|
||||
|
tk.freeze(new Date(2022, 0, 2, 9, 0)) |
||||
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties) |
||||
|
// go forward into next day
|
||||
|
tk.freeze(new Date(2022, 0, 3, 5, 0)) |
||||
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties) |
||||
|
// go forward one hour
|
||||
|
tk.freeze(new Date(2022, 0, 3, 6, 0)) |
||||
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties) |
||||
|
|
||||
|
expect(processor.posthog.capture).toHaveBeenCalledTimes(4) |
||||
|
}) |
||||
|
|
||||
|
it("sends event again after cache expires", async () => { |
||||
|
const processor = new PosthogProcessor("test") |
||||
|
const identity = newIdentity() |
||||
|
const properties = {} |
||||
|
|
||||
|
tk.freeze(new Date(2022, 0, 1, 14, 0)) |
||||
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties) |
||||
|
|
||||
|
await Cache.bustCache( |
||||
|
`${Cache.CacheKeys.EVENTS_RATE_LIMIT}:${Event.SERVED_BUILDER}` |
||||
|
) |
||||
|
|
||||
|
tk.freeze(new Date(2022, 0, 1, 14, 0)) |
||||
|
await processor.processEvent(Event.SERVED_BUILDER, identity, properties) |
||||
|
|
||||
|
expect(processor.posthog.capture).toHaveBeenCalledTimes(2) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
|
}) |
||||
@ -1,40 +0,0 @@ |
|||||
import PosthogProcessor from "../PosthogProcessor" |
|
||||
import { Event, IdentityType, Hosting } from "@budibase/types" |
|
||||
|
|
||||
const newIdentity = () => { |
|
||||
return { |
|
||||
id: "test", |
|
||||
type: IdentityType.USER, |
|
||||
hosting: Hosting.SELF, |
|
||||
environment: "test", |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
describe("PosthogProcessor", () => { |
|
||||
beforeEach(() => { |
|
||||
jest.clearAllMocks() |
|
||||
}) |
|
||||
|
|
||||
describe("processEvent", () => { |
|
||||
it("processes event", () => { |
|
||||
const processor = new PosthogProcessor("test") |
|
||||
|
|
||||
const identity = newIdentity() |
|
||||
const properties = {} |
|
||||
|
|
||||
processor.processEvent(Event.APP_CREATED, identity, properties) |
|
||||
|
|
||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(1) |
|
||||
}) |
|
||||
|
|
||||
it("honours exclusions", () => { |
|
||||
const processor = new PosthogProcessor("test") |
|
||||
|
|
||||
const identity = newIdentity() |
|
||||
const properties = {} |
|
||||
|
|
||||
processor.processEvent(Event.AUTH_SSO_UPDATED, identity, properties) |
|
||||
expect(processor.posthog.capture).toHaveBeenCalledTimes(0) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
Loading…
Reference in new issue