Merge pull request #7194 from Budibase/trigger-served-ping

Trigger served events on ping endpoint
This commit is contained in:
Rory Powell 2022-08-10 12:37:24 +01:00 committed by GitHub
commit d1abb4f7b4
20 changed files with 147 additions and 111 deletions

View File

@ -1,27 +0,0 @@
import { EventEmitter } from "events"
import * as context from "../../context"
import { Identity, Event } from "@budibase/types"
export interface EmittedEvent {
tenantId: string
identity: Identity
appId: string | undefined
properties: any
}
class BBEventEmitter extends EventEmitter {
emitEvent(event: Event, properties: any, identity: Identity) {
const tenantId = context.getTenantId()
const appId = context.getAppId()
const emittedEvent: EmittedEvent = {
tenantId,
identity,
appId,
properties,
}
this.emit(event, emittedEvent)
}
}
export const emitter = new BBEventEmitter()

View File

@ -1 +0,0 @@
export * from "./BBEventEmitter"

View File

@ -2,41 +2,6 @@ import { Event } from "@budibase/types"
import { processors } from "./processors" import { processors } from "./processors"
import * as identification from "./identification" import * as identification from "./identification"
import * as backfill from "./backfill" import * as backfill from "./backfill"
import { emitter, EmittedEvent } from "./emit"
import * as context from "../context"
import * as logging from "../logging"
const USE_EMITTER: any[] = [
Event.SERVED_BUILDER,
Event.SERVED_APP,
Event.SERVED_APP_PREVIEW,
]
for (let event of USE_EMITTER) {
emitter.on(event, async (props: EmittedEvent) => {
try {
await context.doInTenant(props.tenantId, async () => {
if (props.appId) {
await context.doInAppContext(props.appId, async () => {
await processors.processEvent(
event as Event,
props.identity,
props.properties
)
})
} else {
await processors.processEvent(
event as Event,
props.identity,
props.properties
)
}
})
} catch (e) {
logging.logAlert(`Unable to process async event ${event}`, e)
}
})
}
export const publishEvent = async ( export const publishEvent = async (
event: Event, event: Event,
@ -46,11 +11,6 @@ export const publishEvent = async (
// in future this should use async events via a distributed queue. // in future this should use async events via a distributed queue.
const identity = await identification.getCurrentIdentity() const identity = await identification.getCurrentIdentity()
if (USE_EMITTER.includes(event)) {
emitter.emitEvent(event, properties, identity)
return
}
const backfilling = await backfill.isBackfillingEvent(event) const backfilling = await backfill.isBackfillingEvent(event)
// no backfill - send the event and exit // no backfill - send the event and exit
if (!backfilling) { if (!backfilling) {

View File

@ -7,22 +7,26 @@ import {
AppServedEvent, AppServedEvent,
} from "@budibase/types" } from "@budibase/types"
export async function servedBuilder() { export async function servedBuilder(timezone: string) {
const properties: BuilderServedEvent = {} const properties: BuilderServedEvent = {
timezone,
}
await publishEvent(Event.SERVED_BUILDER, properties) await publishEvent(Event.SERVED_BUILDER, properties)
} }
export async function servedApp(app: App) { export async function servedApp(app: App, timezone: string) {
const properties: AppServedEvent = { const properties: AppServedEvent = {
appVersion: app.version, appVersion: app.version,
timezone,
} }
await publishEvent(Event.SERVED_APP, properties) await publishEvent(Event.SERVED_APP, properties)
} }
export async function servedAppPreview(app: App) { export async function servedAppPreview(app: App, timezone: string) {
const properties: AppPreviewServedEvent = { const properties: AppPreviewServedEvent = {
appId: app.appId, appId: app.appId,
appVersion: app.version, appVersion: app.version,
timezone,
} }
await publishEvent(Event.SERVED_APP_PREVIEW, properties) await publishEvent(Event.SERVED_APP_PREVIEW, properties)
} }

View File

@ -3,6 +3,7 @@
import { admin, auth } from "stores/portal" import { admin, auth } from "stores/portal"
import { onMount } from "svelte" import { onMount } from "svelte"
import { CookieUtils, Constants } from "@budibase/frontend-core" import { CookieUtils, Constants } from "@budibase/frontend-core"
import { API } from "api"
let loaded = false let loaded = false
@ -53,6 +54,9 @@
await auth.setOrganisation(urlTenantId) await auth.setOrganisation(urlTenantId)
} }
} }
async function analyticsPing() {
await API.analyticsPing({ source: "builder" })
}
onMount(async () => { onMount(async () => {
try { try {
@ -73,6 +77,9 @@
// being logged in // being logged in
} }
loaded = true loaded = true
// lastly
await analyticsPing()
}) })
$: { $: {

View File

@ -83,6 +83,8 @@
dataLoaded = true dataLoaded = true
if (get(builderStore).inBuilder) { if (get(builderStore).inBuilder) {
builderStore.actions.notifyLoaded() builderStore.actions.notifyLoaded()
} else {
builderStore.actions.analyticsPing({ source: "app" })
} }
}) })
</script> </script>

View File

@ -1,4 +1,5 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api"
import { devToolsStore } from "./devTools.js" import { devToolsStore } from "./devTools.js"
const dispatchEvent = (type, data = {}) => { const dispatchEvent = (type, data = {}) => {
@ -48,6 +49,13 @@ const createBuilderStore = () => {
notifyLoaded: () => { notifyLoaded: () => {
dispatchEvent("preview-loaded") dispatchEvent("preview-loaded")
}, },
analyticsPing: async () => {
try {
await API.analyticsPing({ source: "app" })
} catch (error) {
// Do nothing
}
},
moveComponent: (componentId, destinationComponentId, mode) => { moveComponent: (componentId, destinationComponentId, mode) => {
dispatchEvent("move-component", { dispatchEvent("move-component", {
componentId, componentId,

View File

@ -7,4 +7,11 @@ export const buildAnalyticsEndpoints = API => ({
url: "/api/bbtel", url: "/api/bbtel",
}) })
}, },
analyticsPing: async ({ source }) => {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
return await API.post({
url: "/api/bbtel/ping",
body: { source, timezone },
})
},
}) })

View File

@ -1,4 +1,7 @@
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
import { AnalyticsPingRequest, PingSource } from "@budibase/types"
import { DocumentTypes, isDevAppID } from "../../db/utils"
import { context } from "@budibase/backend-core"
export const isEnabled = async (ctx: any) => { export const isEnabled = async (ctx: any) => {
const enabled = await events.analytics.enabled() const enabled = await events.analytics.enabled()
@ -6,3 +9,27 @@ export const isEnabled = async (ctx: any) => {
enabled, enabled,
} }
} }
export const ping = async (ctx: any) => {
const body = ctx.request.body as AnalyticsPingRequest
switch (body.source) {
case PingSource.APP: {
const db = context.getAppDB({ skip_setup: true })
const appInfo = await db.get(DocumentTypes.APP_METADATA)
let appId = context.getAppId()
if (isDevAppID(appId)) {
await events.serve.servedAppPreview(appInfo, body.timezone)
} else {
await events.serve.servedApp(appInfo, body.timezone)
}
break
}
case PingSource.BUILDER: {
await events.serve.servedBuilder(body.timezone)
break
}
}
ctx.status = 200
}

View File

@ -14,11 +14,10 @@ const env = require("../../../environment")
const { clientLibraryPath } = require("../../../utilities") const { clientLibraryPath } = require("../../../utilities")
const { upload } = require("../../../utilities/fileSystem") const { upload } = require("../../../utilities/fileSystem")
const { attachmentsRelativeURL } = require("../../../utilities") const { attachmentsRelativeURL } = require("../../../utilities")
const { DocumentTypes, isDevAppID } = require("../../../db/utils") const { DocumentTypes } = require("../../../db/utils")
const { getAppDB, getAppId } = require("@budibase/backend-core/context") const { getAppDB, getAppId } = require("@budibase/backend-core/context")
const { setCookie, clearCookie } = require("@budibase/backend-core/utils") const { setCookie, clearCookie } = require("@budibase/backend-core/utils")
const AWS = require("aws-sdk") const AWS = require("aws-sdk")
import { events } from "@budibase/backend-core"
const fs = require("fs") const fs = require("fs")
const { const {
@ -75,9 +74,6 @@ export const toggleBetaUiFeature = async function (ctx: any) {
export const serveBuilder = async function (ctx: any) { export const serveBuilder = async function (ctx: any) {
const builderPath = resolve(TOP_LEVEL_PATH, "builder") const builderPath = resolve(TOP_LEVEL_PATH, "builder")
await send(ctx, ctx.file, { root: builderPath }) await send(ctx, ctx.file, { root: builderPath })
if (ctx.file === "index.html") {
await events.serve.servedBuilder()
}
} }
export const uploadFile = async function (ctx: any) { export const uploadFile = async function (ctx: any) {
@ -126,12 +122,6 @@ export const serveApp = async function (ctx: any) {
// just return the app info for jest to assert on // just return the app info for jest to assert on
ctx.body = appInfo ctx.body = appInfo
} }
if (isDevAppID(appInfo.appId)) {
await events.serve.servedAppPreview(appInfo)
} else {
await events.serve.servedApp(appInfo)
}
} }
export const serveClientLibrary = async function (ctx: any) { export const serveClientLibrary = async function (ctx: any) {

View File

@ -4,5 +4,6 @@ const controller = require("../controllers/analytics")
const router = Router() const router = Router()
router.get("/api/bbtel", controller.isEnabled) router.get("/api/bbtel", controller.isEnabled)
router.post("/api/bbtel/ping", controller.ping)
module.exports = router module.exports = router

View File

@ -0,0 +1,59 @@
const setup = require("./utilities")
const { events, constants, db } = require("@budibase/backend-core")
describe("/static", () => {
let request = setup.getRequest()
let config = setup.getConfig()
let app
const timezone = "Europe/London"
afterAll(setup.afterAll)
beforeEach(async () => {
app = await config.init()
jest.clearAllMocks()
})
describe("/ping", () => {
it("should ping from builder", async () => {
await request
.post("/api/bbtel/ping")
.send({source: "builder", timezone})
.set(config.defaultHeaders())
.expect(200)
expect(events.serve.servedBuilder).toBeCalledTimes(1)
expect(events.serve.servedBuilder).toBeCalledWith(timezone)
expect(events.serve.servedApp).not.toBeCalled()
expect(events.serve.servedAppPreview).not.toBeCalled()
})
it("should ping from app preview", async () => {
await request
.post("/api/bbtel/ping")
.send({source: "app", timezone})
.set(config.defaultHeaders())
.expect(200)
expect(events.serve.servedAppPreview).toBeCalledTimes(1)
expect(events.serve.servedAppPreview).toBeCalledWith(config.getApp(), timezone)
expect(events.serve.servedApp).not.toBeCalled()
})
it("should ping from app", async () => {
const headers = config.defaultHeaders()
headers[constants.Headers.APP_ID] = config.prodAppId
await request
.post("/api/bbtel/ping")
.send({source: "app", timezone})
.set(headers)
.expect(200)
expect(events.serve.servedApp).toBeCalledTimes(1)
expect(events.serve.servedApp).toBeCalledWith(config.getProdApp(), timezone)
expect(events.serve.servedAppPreview).not.toBeCalled()
})
})
})

View File

@ -36,7 +36,6 @@ describe("/static", () => {
.expect(200) .expect(200)
expect(res.text).toContain("<title>Budibase</title>") expect(res.text).toContain("<title>Budibase</title>")
expect(events.serve.servedBuilder).toBeCalledTimes(1)
}) })
}) })
@ -56,9 +55,6 @@ describe("/static", () => {
.expect(200) .expect(200)
expect(res.body.appId).toBe(config.prodAppId) expect(res.body.appId).toBe(config.prodAppId)
expect(events.serve.servedApp).toBeCalledTimes(1)
expect(events.serve.servedApp).toBeCalledWith(res.body)
expect(events.serve.servedAppPreview).not.toBeCalled()
}) })
it("should serve the app by url", async () => { it("should serve the app by url", async () => {
@ -71,9 +67,6 @@ describe("/static", () => {
.expect(200) .expect(200)
expect(res.body.appId).toBe(config.prodAppId) expect(res.body.appId).toBe(config.prodAppId)
expect(events.serve.servedApp).toBeCalledTimes(1)
expect(events.serve.servedApp).toBeCalledWith(res.body)
expect(events.serve.servedAppPreview).not.toBeCalled()
}) })
it("should serve the app preview by id", async () => { it("should serve the app preview by id", async () => {
@ -83,9 +76,6 @@ describe("/static", () => {
.expect(200) .expect(200)
expect(res.body.appId).toBe(config.appId) expect(res.body.appId).toBe(config.appId)
expect(events.serve.servedAppPreview).toBeCalledTimes(1)
expect(events.serve.servedAppPreview).toBeCalledWith(res.body)
expect(events.serve.servedApp).not.toBeCalled()
}) })
}) })

View File

@ -25,6 +25,7 @@ const newid = require("../../db/newid")
const context = require("@budibase/backend-core/context") const context = require("@budibase/backend-core/context")
const { generateDevInfoID, SEPARATOR } = require("@budibase/backend-core/db") const { generateDevInfoID, SEPARATOR } = require("@budibase/backend-core/db")
const { encrypt } = require("@budibase/backend-core/encryption") const { encrypt } = require("@budibase/backend-core/encryption")
const { DocumentTypes } = require("../../db/utils")
const GLOBAL_USER_ID = "us_uuid1" const GLOBAL_USER_ID = "us_uuid1"
const EMAIL = "babs@babs.com" const EMAIL = "babs@babs.com"
@ -53,6 +54,10 @@ class TestConfiguration {
return this.app return this.app
} }
getProdApp() {
return this.prodApp
}
getAppId() { getAppId() {
return this.appId return this.appId
} }
@ -106,19 +111,11 @@ class TestConfiguration {
// UTILS // UTILS
async _req(body, params, controlFunc, opts = { prodApp: false }) { async _req(body, params, controlFunc) {
// create a fake request ctx // create a fake request ctx
const request = {} const request = {}
const appId = this.appId
// set the app id
let appId
if (opts.prodApp) {
appId = this.prodAppId
} else {
appId = this.appId
}
request.appId = appId request.appId = appId
// fake cookies, we don't need them // fake cookies, we don't need them
request.cookies = { set: () => {}, get: () => {} } request.cookies = { set: () => {}, get: () => {} }
request.config = { jwtSecret: env.JWT_SECRET } request.config = { jwtSecret: env.JWT_SECRET }
@ -344,14 +341,10 @@ class TestConfiguration {
await this._req(null, null, controllers.deploy.deployApp) await this._req(null, null, controllers.deploy.deployApp)
const prodAppId = this.getAppId().replace("_dev", "") const prodAppId = this.getAppId().replace("_dev", "")
this.prodAppId = prodAppId this.prodAppId = prodAppId
return context.doInAppContext(prodAppId, async () => { return context.doInAppContext(prodAppId, async () => {
const appPackage = await this._req( const db = context.getProdAppDB()
null, return await db.get(DocumentTypes.APP_METADATA)
{ appId: prodAppId },
controllers.app.fetchAppPackage,
{ prodApp: true }
)
return appPackage.application
}) })
} }

View File

@ -0,0 +1 @@
export * from "./web"

View File

@ -0,0 +1,9 @@
export enum PingSource {
BUILDER = "builder",
APP = "app",
}
export interface AnalyticsPingRequest {
source: PingSource
timezone: string
}

View File

@ -0,0 +1 @@
export * from "./analytics"

View File

@ -2,3 +2,4 @@ export * from "./documents"
export * from "./sdk/events" export * from "./sdk/events"
export * from "./sdk/licensing" export * from "./sdk/licensing"
export * from "./sdk" export * from "./sdk"
export * from "./api"

View File

@ -1,11 +1,15 @@
import { BaseEvent } from "./event" import { BaseEvent } from "./event"
export interface BuilderServedEvent extends BaseEvent {} export interface BuilderServedEvent extends BaseEvent {
timezone: string
}
export interface AppServedEvent extends BaseEvent { export interface AppServedEvent extends BaseEvent {
appVersion: string appVersion: string
timezone: string
} }
export interface AppPreviewServedEvent extends BaseEvent { export interface AppPreviewServedEvent extends BaseEvent {
appVersion: string appVersion: string
timezone: string
} }