diff --git a/.github/workflows/release-develop.yml b/.github/workflows/release-develop.yml
index ce41fcc3e6..cf0d6f848c 100644
--- a/.github/workflows/release-develop.yml
+++ b/.github/workflows/release-develop.yml
@@ -7,6 +7,7 @@ on:
env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
+ INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6316bf1837..7b38a70eb7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,6 +13,7 @@ on:
env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
+ INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
diff --git a/packages/builder/src/App.svelte b/packages/builder/src/App.svelte
index 0624690b27..60051ea043 100644
--- a/packages/builder/src/App.svelte
+++ b/packages/builder/src/App.svelte
@@ -1,16 +1,10 @@
diff --git a/packages/builder/src/analytics.js b/packages/builder/src/analytics.js
deleted file mode 100644
index 5b130a8e6b..0000000000
--- a/packages/builder/src/analytics.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import * as Sentry from "@sentry/browser"
-import posthog from "posthog-js"
-import api from "builderStore/api"
-
-let analyticsEnabled
-const posthogConfigured = process.env.POSTHOG_TOKEN && process.env.POSTHOG_URL
-const sentryConfigured = process.env.SENTRY_DSN
-
-const FEEDBACK_SUBMITTED_KEY = "budibase:feedback_submitted"
-const APP_FIRST_STARTED_KEY = "budibase:first_run"
-const feedbackHours = 12
-
-async function activate() {
- if (analyticsEnabled === undefined) {
- // only the server knows the true NODE_ENV
- // this was an issue as NODE_ENV = 'cypress' on the server,
- // but 'production' on the client
- const response = await api.get("/api/analytics")
- analyticsEnabled = (await response.json()).enabled === true
- }
- if (!analyticsEnabled) return
- if (sentryConfigured) Sentry.init({ dsn: process.env.SENTRY_DSN })
- if (posthogConfigured) {
- posthog.init(process.env.POSTHOG_TOKEN, {
- autocapture: false,
- capture_pageview: false,
- api_host: process.env.POSTHOG_URL,
- })
- posthog.set_config({ persistence: "cookie" })
- }
-}
-
-function identify(id) {
- if (!analyticsEnabled || !id) return
- if (posthogConfigured) posthog.identify(id)
- if (sentryConfigured)
- Sentry.configureScope(scope => {
- scope.setUser({ id: id })
- })
-}
-
-async function identifyByApiKey(apiKey) {
- if (!analyticsEnabled) return true
- try {
- const response = await fetch(
- `https://03gaine137.execute-api.eu-west-1.amazonaws.com/prod/account/id?api_key=${apiKey.trim()}`
- )
- if (response.status === 200) {
- const id = await response.json()
-
- await api.put("/api/keys/userId", { value: id })
- identify(id)
- return true
- }
-
- return false
- } catch (error) {
- console.log(error)
- }
-}
-
-function captureException(err) {
- if (!analyticsEnabled) return
- Sentry.captureException(err)
- captureEvent("Error", { error: err.message ? err.message : err })
-}
-
-function captureEvent(eventName, props = {}) {
- if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return
- props.sourceApp = "builder"
- posthog.capture(eventName, props)
-}
-
-if (!localStorage.getItem(APP_FIRST_STARTED_KEY)) {
- localStorage.setItem(APP_FIRST_STARTED_KEY, Date.now())
-}
-
-const isFeedbackTimeElapsed = sinceDateStr => {
- const sinceDate = parseFloat(sinceDateStr)
- const feedbackMilliseconds = feedbackHours * 60 * 60 * 1000
- return Date.now() > sinceDate + feedbackMilliseconds
-}
-
-function submitFeedback(values) {
- if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return
- localStorage.setItem(FEEDBACK_SUBMITTED_KEY, Date.now())
-
- const prefixedValues = Object.entries(values).reduce((obj, [key, value]) => {
- obj[`feedback_${key}`] = value
- return obj
- }, {})
-
- posthog.capture("Feedback Submitted", prefixedValues)
-}
-
-function requestFeedbackOnDeploy() {
- if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return false
- const lastSubmittedStr = localStorage.getItem(FEEDBACK_SUBMITTED_KEY)
- if (!lastSubmittedStr) return true
- return isFeedbackTimeElapsed(lastSubmittedStr)
-}
-
-function highlightFeedbackIcon() {
- if (!analyticsEnabled || !process.env.POSTHOG_TOKEN) return false
- const lastSubmittedStr = localStorage.getItem(FEEDBACK_SUBMITTED_KEY)
- if (lastSubmittedStr) return isFeedbackTimeElapsed(lastSubmittedStr)
- const firstRunStr = localStorage.getItem(APP_FIRST_STARTED_KEY)
- if (!firstRunStr) return false
- return isFeedbackTimeElapsed(firstRunStr)
-}
-
-// Opt In/Out
-const ifAnalyticsEnabled = func => () => {
- if (analyticsEnabled && process.env.POSTHOG_TOKEN) {
- return func()
- }
-}
-const disabled = () => posthog.has_opted_out_capturing()
-const optIn = () => posthog.opt_in_capturing()
-const optOut = () => posthog.opt_out_capturing()
-
-export default {
- activate,
- identify,
- identifyByApiKey,
- captureException,
- captureEvent,
- requestFeedbackOnDeploy,
- submitFeedback,
- highlightFeedbackIcon,
- disabled: () => {
- if (analyticsEnabled == null) {
- return true
- }
- return ifAnalyticsEnabled(disabled)
- },
- optIn: ifAnalyticsEnabled(optIn),
- optOut: ifAnalyticsEnabled(optOut),
-}
diff --git a/packages/builder/src/analytics/IntercomClient.js b/packages/builder/src/analytics/IntercomClient.js
new file mode 100644
index 0000000000..8cc7e35bbf
--- /dev/null
+++ b/packages/builder/src/analytics/IntercomClient.js
@@ -0,0 +1,94 @@
+export default class IntercomClient {
+ constructor(token) {
+ this.token = token
+ }
+
+ /**
+ * Instantiate intercom using their provided script.
+ */
+ init() {
+ if (!this.token) return
+
+ const token = this.token
+
+ var w = window
+ var ic = w.Intercom
+ if (typeof ic === "function") {
+ ic("reattach_activator")
+ ic("update", w.intercomSettings)
+ } else {
+ var d = document
+ var i = function () {
+ i.c(arguments)
+ }
+ i.q = []
+ i.c = function (args) {
+ i.q.push(args)
+ }
+ w.Intercom = i
+ var l = function () {
+ var s = d.createElement("script")
+ s.type = "text/javascript"
+ s.async = true
+ s.src = "https://widget.intercom.io/widget/" + token
+ var x = d.getElementsByTagName("script")[0]
+ x.parentNode.insertBefore(s, x)
+ }
+ if (document.readyState === "complete") {
+ l()
+ } else if (w.attachEvent) {
+ w.attachEvent("onload", l)
+ } else {
+ w.addEventListener("load", l, false)
+ }
+
+ this.initialised = true
+ }
+ }
+
+ /**
+ * Show the intercom chat bubble.
+ * @param {Object} user - user to identify
+ * @returns Intercom global object
+ */
+ show(user = {}) {
+ if (!this.initialised) return
+
+ return window.Intercom("boot", {
+ app_id: this.token,
+ ...user,
+ })
+ }
+
+ /**
+ * Update intercom user details and messages.
+ * @returns Intercom global object
+ */
+ update() {
+ if (!this.initialised) return
+
+ return window.Intercom("update")
+ }
+
+ /**
+ * Capture analytics events and send them to intercom.
+ * @param {String} event - event identifier
+ * @param {Object} props - properties for the event
+ * @returns Intercom global object
+ */
+ captureEvent(event, props = {}) {
+ if (!this.initialised) return
+
+ return window.Intercom("trackEvent", event, props)
+ }
+
+ /**
+ * Disassociate the user from the current session.
+ * @returns Intercom global object
+ */
+ logout() {
+ if (!this.initialised) return
+
+ return window.Intercom("shutdown")
+ }
+}
diff --git a/packages/builder/src/analytics/PosthogClient.js b/packages/builder/src/analytics/PosthogClient.js
new file mode 100644
index 0000000000..0a1fde42ea
--- /dev/null
+++ b/packages/builder/src/analytics/PosthogClient.js
@@ -0,0 +1,80 @@
+import posthog from "posthog-js"
+import { Events } from "./constants"
+
+export default class PosthogClient {
+ constructor(token, url) {
+ this.token = token
+ this.url = url
+ }
+
+ init() {
+ if (!this.token || !this.url) return
+
+ posthog.init(this.token, {
+ autocapture: false,
+ capture_pageview: false,
+ api_host: this.url,
+ })
+ posthog.set_config({ persistence: "cookie" })
+
+ this.initialised = true
+ }
+
+ /**
+ * Set the posthog context to the current user
+ * @param {String} id - unique user id
+ */
+ identify(id) {
+ if (!this.initialised) return
+
+ posthog.identify(id)
+ }
+
+ /**
+ * Update user metadata associated with current user in posthog
+ * @param {Object} meta - user fields
+ */
+ updateUser(meta) {
+ if (!this.initialised) return
+
+ posthog.people.set(meta)
+ }
+
+ /**
+ * Capture analytics events and send them to posthog.
+ * @param {String} event - event identifier
+ * @param {Object} props - properties for the event
+ */
+ captureEvent(eventName, props) {
+ if (!this.initialised) return
+
+ props.sourceApp = "builder"
+ posthog.capture(eventName, props)
+ }
+
+ /**
+ * Submit NPS feedback to posthog.
+ * @param {Object} values - NPS Values
+ */
+ npsFeedback(values) {
+ if (!this.initialised) return
+
+ localStorage.setItem(Events.NPS.SUBMITTED, Date.now())
+
+ const prefixedFeedback = {}
+ for (let key in values) {
+ prefixedFeedback[`feedback_${key}`] = values[key]
+ }
+
+ posthog.capture(Events.NPS.SUBMITTED, prefixedFeedback)
+ }
+
+ /**
+ * Reset posthog user back to initial state on logout.
+ */
+ logout() {
+ if (!this.initialised) return
+
+ posthog.reset()
+ }
+}
diff --git a/packages/builder/src/analytics/SentryClient.js b/packages/builder/src/analytics/SentryClient.js
new file mode 100644
index 0000000000..2a1f8732e3
--- /dev/null
+++ b/packages/builder/src/analytics/SentryClient.js
@@ -0,0 +1,37 @@
+import * as Sentry from "@sentry/browser"
+
+export default class SentryClient {
+ constructor(dsn) {
+ this.dsn = dsn
+ }
+
+ init() {
+ if (this.dsn) {
+ Sentry.init({ dsn: this.dsn })
+
+ this.initalised = true
+ }
+ }
+
+ /**
+ * Capture an exception and send it to sentry.
+ * @param {Error} err - JS error object
+ */
+ captureException(err) {
+ if (!this.initalised) return
+
+ Sentry.captureException(err)
+ }
+
+ /**
+ * Identify user in sentry.
+ * @param {String} id - Unique user id
+ */
+ identify(id) {
+ if (!this.initalised) return
+
+ Sentry.configureScope(scope => {
+ scope.setUser({ id })
+ })
+ }
+}
diff --git a/packages/builder/src/analytics/constants.js b/packages/builder/src/analytics/constants.js
new file mode 100644
index 0000000000..d38b7bba4f
--- /dev/null
+++ b/packages/builder/src/analytics/constants.js
@@ -0,0 +1,49 @@
+export const Events = {
+ BUILDER: {
+ STARTED: "Builder Started",
+ },
+ COMPONENT: {
+ CREATED: "Added Component",
+ },
+ DATASOURCE: {
+ CREATED: "Datasource Created",
+ UPDATED: "Datasource Updated",
+ },
+ TABLE: {
+ CREATED: "Table Created",
+ },
+ VIEW: {
+ CREATED: "View Created",
+ ADDED_FILTER: "Added View Filter",
+ ADDED_CALCULATE: "Added View Calculate",
+ },
+ SCREEN: {
+ CREATED: "Screen Created",
+ },
+ AUTOMATION: {
+ CREATED: "Automation Created",
+ SAVED: "Automation Saved",
+ BLOCK_ADDED: "Added Automation Block",
+ },
+ NPS: {
+ SUBMITTED: "budibase:feedback_submitted",
+ },
+ APP: {
+ CREATED: "budibase:app_created",
+ PUBLISHED: "budibase:app_published",
+ UNPUBLISHED: "budibase:app_unpublished",
+ },
+ ANALYTICS: {
+ OPT_IN: "budibase:analytics_opt_in",
+ OPT_OUT: "budibase:analytics_opt_out",
+ },
+ USER: {
+ INVITE: "budibase:portal_user_invite",
+ },
+ SMTP: {
+ SAVED: "budibase:smtp_saved",
+ },
+ SSO: {
+ SAVED: "budibase:sso_saved",
+ },
+}
diff --git a/packages/builder/src/analytics/index.js b/packages/builder/src/analytics/index.js
new file mode 100644
index 0000000000..b79ab67e0c
--- /dev/null
+++ b/packages/builder/src/analytics/index.js
@@ -0,0 +1,79 @@
+import api from "builderStore/api"
+import PosthogClient from "./PosthogClient"
+import IntercomClient from "./IntercomClient"
+import SentryClient from "./SentryClient"
+import { Events } from "./constants"
+import { auth } from "stores/portal"
+import { get } from "svelte/store"
+
+const posthog = new PosthogClient(
+ process.env.POSTHOG_TOKEN,
+ process.env.POSTHOG_URL
+)
+const sentry = new SentryClient(process.env.SENTRY_DSN)
+const intercom = new IntercomClient(process.env.INTERCOM_TOKEN)
+
+class AnalyticsHub {
+ constructor() {
+ this.clients = [posthog, sentry, intercom]
+ }
+
+ async activate() {
+ // Setting the analytics env var off in the backend overrides org/tenant settings
+ const analyticsStatus = await api.get("/api/analytics")
+ const json = await analyticsStatus.json()
+
+ // Multitenancy disabled on the backend
+ if (!json.enabled) return
+
+ const tenantId = get(auth).tenantId
+
+ if (tenantId) {
+ const res = await api.get(
+ `/api/global/configs/public?tenantId=${tenantId}`
+ )
+ const orgJson = await res.json()
+
+ // analytics opted out for the tenant
+ if (orgJson.config?.analytics === false) return
+ }
+
+ this.clients.forEach(client => client.init())
+ this.enabled = true
+ }
+
+ identify(id, metadata) {
+ posthog.identify(id)
+ if (metadata) {
+ posthog.updateUser(metadata)
+ }
+ sentry.identify(id)
+ }
+
+ captureException(err) {
+ sentry.captureException(err)
+ }
+
+ captureEvent(eventName, props = {}) {
+ posthog.captureEvent(eventName, props)
+ intercom.captureEvent(eventName, props)
+ }
+
+ showChat(user) {
+ intercom.show(user)
+ }
+
+ submitFeedback(values) {
+ posthog.npsFeedback(values)
+ }
+
+ async logout() {
+ posthog.logout()
+ intercom.logout()
+ }
+}
+
+const analytics = new AnalyticsHub()
+
+export { Events }
+export default analytics
diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js
index 6fecda84c0..f32dedd47e 100644
--- a/packages/builder/src/builderStore/index.js
+++ b/packages/builder/src/builderStore/index.js
@@ -3,7 +3,6 @@ import { getAutomationStore } from "./store/automation"
import { getHostingStore } from "./store/hosting"
import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store"
-import analytics from "analytics"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { findComponent } from "./storeUtils"
@@ -55,13 +54,4 @@ export const mainLayout = derived(store, $store => {
export const selectedAccessRole = writable("BASIC")
-export const initialise = async () => {
- try {
- await analytics.activate()
- analytics.captureEvent("Builder Started")
- } catch (err) {
- console.log(err)
- }
-}
-
export const screenSearchString = writable(null)
diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js
index e60553070b..0a47970d28 100644
--- a/packages/builder/src/builderStore/store/automation/index.js
+++ b/packages/builder/src/builderStore/store/automation/index.js
@@ -2,7 +2,7 @@ import { writable } from "svelte/store"
import api from "../../api"
import Automation from "./Automation"
import { cloneDeep } from "lodash/fp"
-import analytics from "analytics"
+import analytics, { Events } from "analytics"
const automationActions = store => ({
fetch: async () => {
@@ -110,7 +110,7 @@ const automationActions = store => ({
state.selectedBlock = newBlock
return state
})
- analytics.captureEvent("Added Automation Block", {
+ analytics.captureEvent(Events.AUTOMATION.BLOCK_ADDED, {
name: block.name,
})
},
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index 603fa88b09..b7c42003da 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -19,7 +19,7 @@ import {
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
import { FrontendTypes } from "constants"
-import analytics from "analytics"
+import analytics, { Events } from "analytics"
import {
findComponentType,
findComponentParent,
@@ -443,7 +443,7 @@ export const getFrontendStore = () => {
})
// Log event
- analytics.captureEvent("Added Component", {
+ analytics.captureEvent(Events.COMPONENT.CREATED, {
name: componentInstance._component,
})
diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
index 7700a4a1c2..f3273aa5ec 100644
--- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
@@ -4,7 +4,7 @@
import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui"
import { Input, ModalContent, Layout, Body, Icon } from "@budibase/bbui"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
let name
let selectedTrigger
@@ -36,7 +36,7 @@
notifications.success(`Automation ${name} created.`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`)
- analytics.captureEvent("Automation Created", { name })
+ analytics.captureEvent(Events.AUTOMATION.CREATED, { name })
}
$: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)
diff --git a/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte
index 29966ec372..64197c3a77 100644
--- a/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/UpdateAutomationModal.svelte
@@ -2,7 +2,7 @@
import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui"
import { Icon, Input, ModalContent, Modal } from "@budibase/bbui"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
let name
let error = ""
@@ -26,7 +26,7 @@
}
await automationStore.actions.save(updatedAutomation)
notifications.success(`Automation ${name} updated successfully.`)
- analytics.captureEvent("Automation Saved", { name })
+ analytics.captureEvent(Events.AUTOMATION.SAVED, { name })
hide()
}
diff --git a/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte
index 660a822898..50d44eca88 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CalculateModal.svelte
@@ -1,7 +1,7 @@
diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte
index 61777c0b7e..2f6ec51233 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CreateViewModal.svelte
@@ -3,7 +3,7 @@
import { goto } from "@roxi/routify"
import { views as viewsStore } from "stores/backend"
import { tables } from "stores/backend"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
let name
let field
@@ -21,7 +21,7 @@
field,
})
notifications.success(`View ${name} created`)
- analytics.captureEvent("View Created", { name })
+ analytics.captureEvent(Events.VIEW.CREATED, { name })
$goto(`../../view/${name}`)
}
diff --git a/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte b/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte
index 170bb75142..9c6f4956b0 100644
--- a/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/FilterModal.svelte
@@ -11,7 +11,7 @@
Icon,
} from "@budibase/bbui"
import { tables, views } from "stores/backend"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
const CONDITIONS = [
{
@@ -65,7 +65,7 @@
function saveView() {
views.save(view)
notifications.success(`View ${view.name} saved.`)
- analytics.captureEvent("Added View Filter", {
+ analytics.captureEvent(Events.VIEW.ADDED_FILTER, {
filters: JSON.stringify(view.filters),
})
}
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte
index 9cdd893230..e7affb30c4 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte
@@ -5,7 +5,7 @@
import { Input, Label, ModalContent, Modal, Context } from "@budibase/bbui"
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
import { getContext } from "svelte"
const modalContext = getContext(Context.Modal)
@@ -45,7 +45,7 @@
plus,
})
notifications.success(`Datasource ${name} created successfully.`)
- analytics.captureEvent("Datasource Created", { name, type })
+ analytics.captureEvent(Events.DATASOURCE.CREATED, { name, type })
// Navigate to new datasource
$goto(`./datasource/${response._id}`)
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte b/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte
index f93af59a38..28625aa86e 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/modals/UpdateDatasourceModal.svelte
@@ -2,7 +2,7 @@
import { datasources } from "stores/backend"
import { notifications } from "@budibase/bbui"
import { Input, ModalContent, Modal } from "@budibase/bbui"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
let error = ""
let modal
@@ -35,7 +35,7 @@
}
await datasources.save(updatedDatasource)
notifications.success(`Datasource ${name} updated successfully.`)
- analytics.captureEvent("Datasource Updated", updatedDatasource)
+ analytics.captureEvent(Events.DATASOURCE.UPDATED, updatedDatasource)
hide()
}
diff --git a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte
index b59e5cda5e..dd8876be27 100644
--- a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte
+++ b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte
@@ -12,7 +12,7 @@
Layout,
} from "@budibase/bbui"
import TableDataImport from "../TableDataImport.svelte"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
import screenTemplates from "builderStore/store/screenTemplates"
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
@@ -67,7 +67,7 @@
// Create table
const table = await tables.save(newTable)
notifications.success(`Table ${name} created successfully.`)
- analytics.captureEvent("Table Created", { name })
+ analytics.captureEvent(Events.TABLE.CREATED, { name })
// Create auto screens
if (createAutoscreens) {
diff --git a/packages/builder/src/components/deploy/DeployModal.svelte b/packages/builder/src/components/deploy/DeployModal.svelte
index 4daa16c7c4..3dcf0c27b1 100644
--- a/packages/builder/src/components/deploy/DeployModal.svelte
+++ b/packages/builder/src/components/deploy/DeployModal.svelte
@@ -2,7 +2,8 @@
import { onMount, onDestroy } from "svelte"
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
import api from "builderStore/api"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
+ import { store } from "builderStore"
const DeploymentStatus = {
SUCCESS: "SUCCESS",
@@ -23,6 +24,9 @@
if (response.status !== 200) {
throw new Error(`status ${response.status}`)
} else {
+ analytics.captureEvent(Events.APP.PUBLISHED, {
+ appId: $store.appId,
+ })
notifications.success(`Application published successfully`)
}
} catch (err) {
diff --git a/packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte b/packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte
index ed0c764956..e02f9d87e5 100644
--- a/packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte
+++ b/packages/builder/src/components/design/NavigationPanel/NewScreenModal.svelte
@@ -4,7 +4,7 @@
import { roles } from "stores/backend"
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
const CONTAINER = "@budibase/standard-components/container"
@@ -66,7 +66,7 @@
if (templateIndex !== undefined) {
const template = templates[templateIndex]
- analytics.captureEvent("Screen Created", {
+ analytics.captureEvent(Events.SCREEN.CREATED, {
template: template.id || template.name,
})
}
diff --git a/packages/builder/src/components/start/CreateAppModal.svelte b/packages/builder/src/components/start/CreateAppModal.svelte
index 4310d3322e..9ce9d746d7 100644
--- a/packages/builder/src/components/start/CreateAppModal.svelte
+++ b/packages/builder/src/components/start/CreateAppModal.svelte
@@ -12,7 +12,7 @@
import { admin } from "stores/portal"
import { string, mixed, object } from "yup"
import api, { get, post } from "builderStore/api"
- import analytics from "analytics"
+ import analytics, { Events } from "analytics"
import { onMount } from "svelte"
import { capitalise } from "helpers"
import { goto } from "@roxi/routify"
@@ -98,9 +98,9 @@
throw new Error(appJson.message)
}
- analytics.captureEvent("App Created", {
+ analytics.captureEvent(Events.APP.CREATED, {
name: $values.name,
- appId: appJson._id,
+ appId: appJson.instance._id,
template,
})
diff --git a/packages/builder/src/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte
index 783e5a4903..f9f2b34578 100644
--- a/packages/builder/src/pages/builder/auth/login.svelte
+++ b/packages/builder/src/pages/builder/auth/login.svelte
@@ -29,6 +29,7 @@
username,
password,
})
+
if ($auth?.user?.forceResetPassword) {
$goto("./reset")
} else {
diff --git a/packages/builder/src/pages/builder/portal/apps/index.svelte b/packages/builder/src/pages/builder/portal/apps/index.svelte
index a18ec6a8bd..d84b327e90 100644
--- a/packages/builder/src/pages/builder/portal/apps/index.svelte
+++ b/packages/builder/src/pages/builder/portal/apps/index.svelte
@@ -15,8 +15,7 @@
} from "@budibase/bbui"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
- import api, { del } from "builderStore/api"
- import analytics from "analytics"
+ import { del } from "builderStore/api"
import { onMount } from "svelte"
import { apps, auth, admin } from "stores/portal"
import download from "downloadjs"
@@ -66,14 +65,6 @@
}
}
- const checkKeys = async () => {
- const response = await api.get(`/api/keys/`)
- const keys = await response.json()
- if (keys.userId) {
- analytics.identify(keys.userId)
- }
- }
-
const initiateAppCreation = () => {
creationModal.show()
creatingApp = true
@@ -188,7 +179,6 @@
}
onMount(async () => {
- checkKeys()
await apps.load()
loaded = true
})
diff --git a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte
index 48d9da18f9..c2445e14ae 100644
--- a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte
+++ b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte
@@ -23,6 +23,7 @@
import api from "builderStore/api"
import { organisation, auth, admin } from "stores/portal"
import { uuid } from "builderStore/uuid"
+ import analytics, { Events } from "analytics"
$: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy
@@ -209,6 +210,7 @@
providers[res.type]._id = res._id
})
notifications.success(`Settings saved.`)
+ analytics.captureEvent(Events.SSO.SAVED)
})
.catch(err => {
notifications.error(`Failed to update auth settings. ${err}`)
diff --git a/packages/builder/src/pages/builder/portal/manage/email/index.svelte b/packages/builder/src/pages/builder/portal/manage/email/index.svelte
index 76d98ed545..5a78623b81 100644
--- a/packages/builder/src/pages/builder/portal/manage/email/index.svelte
+++ b/packages/builder/src/pages/builder/portal/manage/email/index.svelte
@@ -16,6 +16,7 @@
import { email } from "stores/portal"
import api from "builderStore/api"
import { cloneDeep } from "lodash/fp"
+ import analytics, { Events } from "analytics"
const ConfigTypes = {
SMTP: "smtp",
@@ -69,6 +70,7 @@
smtpConfig._rev = json._rev
smtpConfig._id = json._id
notifications.success(`Settings saved.`)
+ analytics.captureEvent(Events.SMTP.SAVED)
}
}
diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte
index 9504f73b68..25a69af1c8 100644
--- a/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte
+++ b/packages/builder/src/pages/builder/portal/manage/users/_components/AddUserModal.svelte
@@ -10,6 +10,7 @@
} from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal"
+ import analytics, { Events } from "analytics"
export let disabled
@@ -25,6 +26,7 @@
notifications.error(res.message)
} else {
notifications.success(res.message)
+ analytics.captureEvent(Events.USER.INVITE, { type: selected })
}
}
diff --git a/packages/builder/src/pages/builder/portal/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte
index be8b60e6e7..79eaebb28b 100644
--- a/packages/builder/src/pages/builder/portal/settings/organisation.svelte
+++ b/packages/builder/src/pages/builder/portal/settings/organisation.svelte
@@ -25,7 +25,7 @@
}
const values = writable({
- analytics: !analytics.disabled(),
+ analytics: analytics.enabled,
company: $organisation.company,
platformUrl: $organisation.platformUrl,
logo: $organisation.logoUrl
@@ -48,13 +48,6 @@
async function saveConfig() {
loading = true
- // Set analytics preference
- if ($values.analytics) {
- analytics.optIn()
- } else {
- analytics.optOut()
- }
-
// Upload logo if required
if ($values.logo && !$values.logo.url) {
await uploadLogo($values.logo)
@@ -64,6 +57,7 @@
const config = {
company: $values.company ?? "",
platformUrl: $values.platformUrl ?? "",
+ analytics: $values.analytics,
}
// remove logo if required
if (!$values.logo) {
diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js
index fe8f87cfb2..e33a1f22ac 100644
--- a/packages/builder/src/stores/portal/auth.js
+++ b/packages/builder/src/stores/portal/auth.js
@@ -1,6 +1,7 @@
import { derived, writable, get } from "svelte/store"
import api from "../../builderStore/api"
import { admin } from "stores/portal"
+import analytics from "analytics"
export function createAuthStore() {
const auth = writable({
@@ -49,6 +50,21 @@ export function createAuthStore() {
}
return store
})
+
+ if (user) {
+ analytics.activate().then(() => {
+ analytics.identify(user._id, user)
+ if (user.size === "100+" || user.size === "10000+") {
+ analytics.showChat({
+ email: user.email,
+ created_at: user.createdAt || Date.now(),
+ name: user.name,
+ user_id: user._id,
+ tenant: user.tenantId,
+ })
+ }
+ })
+ }
}
async function setOrganisation(tenantId) {
diff --git a/packages/builder/vite.config.js b/packages/builder/vite.config.js
index d8b8dbba1d..12b45e7cf8 100644
--- a/packages/builder/vite.config.js
+++ b/packages/builder/vite.config.js
@@ -22,6 +22,9 @@ export default ({ mode }) => {
isProduction ? "production" : "development"
),
"process.env.POSTHOG_TOKEN": JSON.stringify(process.env.POSTHOG_TOKEN),
+ "process.env.INTERCOM_TOKEN": JSON.stringify(
+ process.env.INTERCOM_TOKEN
+ ),
"process.env.POSTHOG_URL": JSON.stringify(process.env.POSTHOG_URL),
"process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN),
}),
diff --git a/packages/server/package.json b/packages/server/package.json
index 80ba5bb44a..bacadf10b5 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -13,7 +13,7 @@
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
"test": "jest --coverage --maxWorkers=2",
"test:watch": "jest --watch",
- "predocker": "copyfiles -f ../client/dist/budibase-client.js ../client/manifest.json client",
+ "predocker": "copyfiles -f ../client/dist/budibase-client.js ../standard-components/manifest.json client",
"build:docker": "yarn run predocker && docker build . -t app-service",
"run:docker": "node dist/index.js",
"dev:stack:up": "node scripts/dev/manage.js up",
@@ -23,10 +23,9 @@
"format": "prettier --config ../../.prettierrc.json 'src/**/*.ts' --write",
"lint": "eslint --fix src/",
"lint:fix": "yarn run format && yarn run lint",
+ "initialise": "node scripts/initialise.js",
"multi:enable": "node scripts/multiTenancy.js enable",
- "multi:disable": "node scripts/multiTenancy.js disable",
- "selfhost:enable": "node scripts/selfhost.js enable",
- "selfhost:disable": "node scripts/selfhost.js disable"
+ "multi:disable": "node scripts/multiTenancy.js disable"
},
"jest": {
"preset": "ts-jest",
@@ -49,8 +48,7 @@
"!src/automations/tests/**/*",
"!src/utilities/fileProcessor.js",
"!src/utilities/fileSystem/**/*",
- "!src/utilities/redis.js",
- "!src/api/controllers/row/internalSearch.js"
+ "!src/utilities/redis.js"
],
"coverageReporters": [
"lcov",
@@ -98,7 +96,7 @@
"lodash": "4.17.21",
"mongodb": "3.6.3",
"mssql": "6.2.3",
- "mysql": "^2.18.1",
+ "mysql": "2.18.1",
"node-fetch": "2.6.0",
"open": "7.3.0",
"pg": "8.5.1",
diff --git a/packages/server/src/api/controllers/analytics.js b/packages/server/src/api/controllers/analytics.js
index d6e1a9ce5b..eb64bc87b9 100644
--- a/packages/server/src/api/controllers/analytics.js
+++ b/packages/server/src/api/controllers/analytics.js
@@ -2,6 +2,6 @@ const env = require("../../environment")
exports.isEnabled = async function (ctx) {
ctx.body = {
- enabled: env.ENABLE_ANALYTICS === "true",
+ enabled: !env.SELF_HOSTED && env.ENABLE_ANALYTICS === "true",
}
}
diff --git a/packages/worker/src/api/controllers/global/users.js b/packages/worker/src/api/controllers/global/users.js
index c8382d4189..1375240f34 100644
--- a/packages/worker/src/api/controllers/global/users.js
+++ b/packages/worker/src/api/controllers/global/users.js
@@ -68,6 +68,7 @@ async function saveUser(
_id = _id || generateGlobalUserID()
user = {
+ createdAt: Date.now(),
...dbUser,
...user,
_id,
@@ -148,6 +149,7 @@ exports.adminUser = async ctx => {
const user = {
email: email,
password: password,
+ createdAt: Date.now(),
roles: {},
builder: {
global: true,