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