From 184061a56b9cf2f3c76288f0be77d89bc5c05600 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 21 Sep 2021 11:47:14 +0100 Subject: [PATCH 1/9] analytics clients --- .github/workflows/release-develop.yml | 1 + .github/workflows/release.yml | 1 + packages/auth/src/objectStore/index.js | 1 + packages/builder/src/analytics.js | 139 ----------------- .../builder/src/analytics/IntercomClient.js | 66 ++++++++ .../builder/src/analytics/PosthogClient.js | 144 ++++++++++++++++++ .../builder/src/analytics/SentryClient.js | 39 +++++ packages/builder/src/analytics/constants.js | 49 ++++++ packages/builder/src/analytics/index.js | 76 +++++++++ packages/builder/src/builderStore/index.js | 4 +- .../src/components/deploy/DeployModal.svelte | 6 +- .../components/start/CreateAppModal.svelte | 6 +- .../src/pages/builder/auth/login.svelte | 13 +- .../pages/builder/portal/apps/index.svelte | 6 +- .../builder/portal/manage/auth/index.svelte | 2 + .../builder/portal/manage/email/index.svelte | 2 + .../users/_components/AddUserModal.svelte | 2 + .../portal/settings/organisation.svelte | 2 +- packages/builder/vite.config.js | 3 + 19 files changed, 412 insertions(+), 150 deletions(-) delete mode 100644 packages/builder/src/analytics.js create mode 100644 packages/builder/src/analytics/IntercomClient.js create mode 100644 packages/builder/src/analytics/PosthogClient.js create mode 100644 packages/builder/src/analytics/SentryClient.js create mode 100644 packages/builder/src/analytics/constants.js create mode 100644 packages/builder/src/analytics/index.js 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 aaee3923ef..00e7f9a795 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.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/packages/auth/src/objectStore/index.js b/packages/auth/src/objectStore/index.js index 81bdd06b62..0f1307c39d 100644 --- a/packages/auth/src/objectStore/index.js +++ b/packages/auth/src/objectStore/index.js @@ -73,6 +73,7 @@ exports.ObjectStore = bucket => { AWS.config.update({ accessKeyId: env.MINIO_ACCESS_KEY, secretAccessKey: env.MINIO_SECRET_KEY, + region: env.AWS_REGION }) const config = { s3ForcePathStyle: true, 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..2cabeb97df --- /dev/null +++ b/packages/builder/src/analytics/IntercomClient.js @@ -0,0 +1,66 @@ +export default class IntercomClient { + constructor(token) { + this.token = token + } + + 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(user = {}) { + if (!this.initialised) return + + return window.Intercom("boot", { + app_id: this.token, + ...user + }) + } + + update() { + if (!this.initialised) return + + return window.Intercom("update") + } + + captureEvent(event, props = {}) { + if (!this.initialised) return + + window.Intercom("trackEvent", event, props); + } +} diff --git a/packages/builder/src/analytics/PosthogClient.js b/packages/builder/src/analytics/PosthogClient.js new file mode 100644 index 0000000000..9a98520c91 --- /dev/null +++ b/packages/builder/src/analytics/PosthogClient.js @@ -0,0 +1,144 @@ +import posthog from "posthog-js" +import { Events } from "./constants" + +// let analyticsEnabled +// const posthogConfigured = process.env.POSTHOG_TOKEN && process.env.POSTHOG_URL + +// const FEEDBACK_SUBMITTED_KEY = "budibase:feedback_submitted" +// const APP_FIRST_STARTED_KEY = "budibase:first_run" +// const feedbackHours = 12 + +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 + } + + disabled() { + return posthog.has_opted_out_capturing() + } + + optIn() { + return posthog.opt_in_capturing() + } + + optOut() { + return posthog.opt_out_capturing() + } + + identify(id) { + if (!this.initialised) return + + posthog.identify(id) + } + + updateUser(meta) { + if (!this.initialised) return + + posthog.people.set(meta) + } + + captureException(err) { + if (!this.initialised) return + + this.captureEvent("Error", { error: err.message ? err.message : err }) + } + + captureEvent(eventName, props) { + if (!this.initialised) return + + props.sourceApp = "builder" + posthog.capture(eventName, props) + } + + 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) + } +} + +// 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()) +// } + +// 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 { +// init, +// identify, +// captureException, +// captureEvent, +// submitFeedback, +// highlightFeedbackIcon, +// disabled: () => { +// if (analyticsEnabled == null) { +// return true +// } +// return ifAnalyticsEnabled(disabled) +// }, +// optIn: ifAnalyticsEnabled(optIn), +// optOut: ifAnalyticsEnabled(optOut), +// } diff --git a/packages/builder/src/analytics/SentryClient.js b/packages/builder/src/analytics/SentryClient.js new file mode 100644 index 0000000000..11377d254f --- /dev/null +++ b/packages/builder/src/analytics/SentryClient.js @@ -0,0 +1,39 @@ +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 + } + } + + captureException(err) { + if (!this.initalised) return + + Sentry.captureException(err) + } + + identify(id) { + Sentry.configureScope(scope => { + scope.setUser({ id }) + }) + } +} + +// export function init() { +// if (process.env.SENTRY_DSN) { +// Sentry.init({ dsn: process.env.SENTRY_DSN }) +// } +// } + +// export function captureException(err) { +// // if (!analyticsEnabled) return +// Sentry.captureException(err) +// // captureEvent("Error", { error: err.message ? err.message : err }) +// } diff --git a/packages/builder/src/analytics/constants.js b/packages/builder/src/analytics/constants.js new file mode 100644 index 0000000000..1102ad3ce4 --- /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: "Added Component", + 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..9fd8f82668 --- /dev/null +++ b/packages/builder/src/analytics/index.js @@ -0,0 +1,76 @@ +import api from "builderStore/api" +import PosthogClient from "./PosthogClient" +import IntercomClient from "./IntercomClient" +import SentryClient from "./SentryClient" +import { Events } from "./constants" + +// const posthog = new PosthogClient( +// process.env.POSTHOG_TOKEN, +// process.env.POSTHOG_URL +// ) +const posthog = new PosthogClient( + "phc_yGOn4i7jWKaCTapdGR6lfA4AvmuEQ2ijn5zAVSFYPlS", + "https://app.posthog.com" +) +// const sentry = new SentryClient(process.env.SENTRY_DSN) +const sentry = new SentryClient("https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131") +// const intercom = new IntercomClient(process.env.INTERCOM_TOKEN) +const intercom = new IntercomClient("qz2sxfuv") + + +class AnalyticsHub { + constructor() { + this.clients = [posthog, sentry, intercom] + } + + async activate() { + const analyticsStatus = await api.get("/api/analytics") + const json = await analyticsStatus.json() + + if (json.enabled) { + this.clients.forEach(client => client.init()) + } + + this.enabled = json.enabled + } + + optIn() { + this.captureEvent(Events.ANALYTICS.OPT_IN) + this.clients.forEach(client => client.optIn()) + } + + optOut() { + this.captureEvent(Events.ANALYTICS.OPT_OUT) + this.clients.forEach(client => client.optOut()) + } + + 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) + } +} + +const analytics = new AnalyticsHub() + +export { Events } +export default analytics \ No newline at end of file diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index 6fecda84c0..fb888b6fc9 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -3,7 +3,7 @@ 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 analytics, { Events } from "analytics" import { FrontendTypes, LAYOUT_NAMES } from "../constants" import { findComponent } from "./storeUtils" @@ -58,7 +58,7 @@ export const selectedAccessRole = writable("BASIC") export const initialise = async () => { try { await analytics.activate() - analytics.captureEvent("Builder Started") + analytics.captureEvent(Events.BUILDER.STARTED) } catch (err) { console.log(err) } diff --git a/packages/builder/src/components/deploy/DeployModal.svelte b/packages/builder/src/components/deploy/DeployModal.svelte index 4daa16c7c4..5500aff866 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/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..568a0550a3 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -15,6 +15,7 @@ import OIDCButton from "./_components/OIDCButton.svelte" import Logo from "assets/bb-emblem.svg" import { onMount } from "svelte" + import analytics from "analytics" let username = "" let password = "" @@ -25,10 +26,20 @@ async function login() { try { - await auth.login({ + const { user } = await auth.login({ username, password, }) + + analytics.identify(user._id, user) + analytics.showChat({ + email: user.email, + created_at: Date.now(), + name: user.name, + user_id: user._id, + tenant: user.tenantId + }) + 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..319a00f471 100644 --- a/packages/builder/src/pages/builder/portal/apps/index.svelte +++ b/packages/builder/src/pages/builder/portal/apps/index.svelte @@ -69,9 +69,9 @@ const checkKeys = async () => { const response = await api.get(`/api/keys/`) const keys = await response.json() - if (keys.userId) { - analytics.identify(keys.userId) - } + // if (keys.userId) { + // analytics.identify(keys.userId) + // } } const initiateAppCreation = () => { 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..be5d6db746 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) } } diff --git a/packages/builder/src/pages/builder/portal/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte index be8b60e6e7..3d149e6e60 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 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), }), From 0a7512a4d9a72beb880898dbad3a7c00e500fa73 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 21 Sep 2021 20:21:15 +0100 Subject: [PATCH 2/9] changed events to constants, instantiated analytics client at right time --- .github/workflows/release.yml | 14 ++- packages/auth/src/objectStore/index.js | 2 +- packages/builder/src/App.svelte | 6 -- .../builder/src/analytics/IntercomClient.js | 10 +- .../builder/src/analytics/PosthogClient.js | 90 ++---------------- .../builder/src/analytics/SentryClient.js | 14 +-- packages/builder/src/analytics/constants.js | 94 +++++++++---------- packages/builder/src/analytics/index.js | 53 ++++++----- packages/builder/src/builderStore/index.js | 10 -- .../builderStore/store/automation/index.js | 4 +- .../src/builderStore/store/frontend.js | 4 +- .../CreateAutomationModal.svelte | 4 +- .../UpdateAutomationModal.svelte | 4 +- .../DataTable/modals/CalculateModal.svelte | 4 +- .../DataTable/modals/CreateViewModal.svelte | 4 +- .../DataTable/modals/FilterModal.svelte | 4 +- .../modals/CreateDatasourceModal.svelte | 4 +- .../modals/UpdateDatasourceModal.svelte | 4 +- .../modals/CreateTableModal.svelte | 4 +- .../src/components/deploy/DeployModal.svelte | 2 +- .../NavigationPanel/NewScreenModal.svelte | 4 +- .../src/pages/builder/auth/login.svelte | 12 +-- .../pages/builder/portal/apps/index.svelte | 12 +-- .../portal/settings/organisation.svelte | 8 +- packages/builder/src/stores/portal/auth.js | 14 +++ packages/server/package.json | 22 ++--- 26 files changed, 156 insertions(+), 251 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00e7f9a795..7e0685c2c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,19 @@ jobs: uses: "WyriHaximus/github-action-get-previous-tag@v1" - name: Build/release Docker images - run: | + if: ${{ github.event.inputs.release_self_host != 'Y' }} + run: | + docker login -u $DOCKER_USER -p $DOCKER_PASSWORD + yarn build + yarn build:docker + env: + DOCKER_USER: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_API_KEY }} + BUDIBASE_RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }} + + - name: Build/release Docker images (Self Host) + if: ${{ github.event.inputs.release_self_host == 'Y' }} + run: | docker login -u $DOCKER_USER -p $DOCKER_PASSWORD yarn build yarn build:docker diff --git a/packages/auth/src/objectStore/index.js b/packages/auth/src/objectStore/index.js index 0f1307c39d..9f271ad80e 100644 --- a/packages/auth/src/objectStore/index.js +++ b/packages/auth/src/objectStore/index.js @@ -73,7 +73,7 @@ exports.ObjectStore = bucket => { AWS.config.update({ accessKeyId: env.MINIO_ACCESS_KEY, secretAccessKey: env.MINIO_SECRET_KEY, - region: env.AWS_REGION + region: env.AWS_REGION, }) const config = { s3ForcePathStyle: true, 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/IntercomClient.js b/packages/builder/src/analytics/IntercomClient.js index 2cabeb97df..10c2f99695 100644 --- a/packages/builder/src/analytics/IntercomClient.js +++ b/packages/builder/src/analytics/IntercomClient.js @@ -48,7 +48,7 @@ export default class IntercomClient { return window.Intercom("boot", { app_id: this.token, - ...user + ...user, }) } @@ -61,6 +61,12 @@ export default class IntercomClient { captureEvent(event, props = {}) { if (!this.initialised) return - window.Intercom("trackEvent", event, props); + window.Intercom("trackEvent", event, props) + } + + logout() { + if (!this.initialised) return + + window.Intercom("shutdown") } } diff --git a/packages/builder/src/analytics/PosthogClient.js b/packages/builder/src/analytics/PosthogClient.js index 9a98520c91..8387c310d3 100644 --- a/packages/builder/src/analytics/PosthogClient.js +++ b/packages/builder/src/analytics/PosthogClient.js @@ -1,13 +1,6 @@ import posthog from "posthog-js" import { Events } from "./constants" -// let analyticsEnabled -// const posthogConfigured = process.env.POSTHOG_TOKEN && process.env.POSTHOG_URL - -// const FEEDBACK_SUBMITTED_KEY = "budibase:feedback_submitted" -// const APP_FIRST_STARTED_KEY = "budibase:first_run" -// const feedbackHours = 12 - export default class PosthogClient { constructor(token, url) { this.token = token @@ -27,18 +20,6 @@ export default class PosthogClient { this.initialised = true } - disabled() { - return posthog.has_opted_out_capturing() - } - - optIn() { - return posthog.opt_in_capturing() - } - - optOut() { - return posthog.opt_out_capturing() - } - identify(id) { if (!this.initialised) return @@ -76,69 +57,10 @@ export default class PosthogClient { posthog.capture(Events.NPS.SUBMITTED, prefixedFeedback) } + + logout() { + if (!this.initialised) return + + posthog.reset() + } } - -// 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()) -// } - -// 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 { -// init, -// identify, -// captureException, -// captureEvent, -// submitFeedback, -// highlightFeedbackIcon, -// disabled: () => { -// if (analyticsEnabled == null) { -// return true -// } -// return ifAnalyticsEnabled(disabled) -// }, -// optIn: ifAnalyticsEnabled(optIn), -// optOut: ifAnalyticsEnabled(optOut), -// } diff --git a/packages/builder/src/analytics/SentryClient.js b/packages/builder/src/analytics/SentryClient.js index 11377d254f..b871ea15d9 100644 --- a/packages/builder/src/analytics/SentryClient.js +++ b/packages/builder/src/analytics/SentryClient.js @@ -20,20 +20,10 @@ export default class SentryClient { } identify(id) { + if (!this.initalised) return + Sentry.configureScope(scope => { scope.setUser({ id }) }) } } - -// export function init() { -// if (process.env.SENTRY_DSN) { -// Sentry.init({ dsn: process.env.SENTRY_DSN }) -// } -// } - -// export function captureException(err) { -// // if (!analyticsEnabled) return -// Sentry.captureException(err) -// // captureEvent("Error", { error: err.message ? err.message : err }) -// } diff --git a/packages/builder/src/analytics/constants.js b/packages/builder/src/analytics/constants.js index 1102ad3ce4..d38b7bba4f 100644 --- a/packages/builder/src/analytics/constants.js +++ b/packages/builder/src/analytics/constants.js @@ -1,49 +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: "Added Component", - 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" - } + 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 index 9fd8f82668..b79ab67e0c 100644 --- a/packages/builder/src/analytics/index.js +++ b/packages/builder/src/analytics/index.js @@ -3,20 +3,15 @@ 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 posthog = new PosthogClient( - "phc_yGOn4i7jWKaCTapdGR6lfA4AvmuEQ2ijn5zAVSFYPlS", - "https://app.posthog.com" + process.env.POSTHOG_TOKEN, + process.env.POSTHOG_URL ) -// const sentry = new SentryClient(process.env.SENTRY_DSN) -const sentry = new SentryClient("https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131") -// const intercom = new IntercomClient(process.env.INTERCOM_TOKEN) -const intercom = new IntercomClient("qz2sxfuv") - +const sentry = new SentryClient(process.env.SENTRY_DSN) +const intercom = new IntercomClient(process.env.INTERCOM_TOKEN) class AnalyticsHub { constructor() { @@ -24,24 +19,27 @@ class AnalyticsHub { } 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() - if (json.enabled) { - this.clients.forEach(client => client.init()) + // 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.enabled = json.enabled - } - - optIn() { - this.captureEvent(Events.ANALYTICS.OPT_IN) - this.clients.forEach(client => client.optIn()) - } - - optOut() { - this.captureEvent(Events.ANALYTICS.OPT_OUT) - this.clients.forEach(client => client.optOut()) + this.clients.forEach(client => client.init()) + this.enabled = true } identify(id, metadata) { @@ -68,9 +66,14 @@ class AnalyticsHub { submitFeedback(values) { posthog.npsFeedback(values) } + + async logout() { + posthog.logout() + intercom.logout() + } } const analytics = new AnalyticsHub() export { Events } -export default analytics \ No newline at end of file +export default analytics diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index fb888b6fc9..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, { Events } 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(Events.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 6580cd0b87..33a2f79b6f 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 5500aff866..3dcf0c27b1 100644 --- a/packages/builder/src/components/deploy/DeployModal.svelte +++ b/packages/builder/src/components/deploy/DeployModal.svelte @@ -25,7 +25,7 @@ throw new Error(`status ${response.status}`) } else { analytics.captureEvent(Events.APP.PUBLISHED, { - appId: $store.appId + appId: $store.appId, }) notifications.success(`Application published successfully`) } 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/pages/builder/auth/login.svelte b/packages/builder/src/pages/builder/auth/login.svelte index 568a0550a3..f9f2b34578 100644 --- a/packages/builder/src/pages/builder/auth/login.svelte +++ b/packages/builder/src/pages/builder/auth/login.svelte @@ -15,7 +15,6 @@ import OIDCButton from "./_components/OIDCButton.svelte" import Logo from "assets/bb-emblem.svg" import { onMount } from "svelte" - import analytics from "analytics" let username = "" let password = "" @@ -26,20 +25,11 @@ async function login() { try { - const { user } = await auth.login({ + await auth.login({ username, password, }) - analytics.identify(user._id, user) - analytics.showChat({ - email: user.email, - created_at: Date.now(), - name: user.name, - user_id: user._id, - tenant: user.tenantId - }) - 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 319a00f471..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/settings/organisation.svelte b/packages/builder/src/pages/builder/portal/settings/organisation.svelte index 3d149e6e60..79eaebb28b 100644 --- a/packages/builder/src/pages/builder/portal/settings/organisation.svelte +++ b/packages/builder/src/pages/builder/portal/settings/organisation.svelte @@ -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..22f7fedca4 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,19 @@ export function createAuthStore() { } return store }) + + if (user) { + analytics.activate().then(() => { + analytics.identify(user._id, user) + // TODO: check if the user is a certain org size + // analytics.showChat({ + // email: user.email, + // name: user.name, + // user_id: user._id, + // tenant: user.tenantId + // }) + }) + } } async function setOrganisation(tenantId) { diff --git a/packages/server/package.json b/packages/server/package.json index 6d6b05752e..fb2e522b87 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "0.9.125-alpha.18", + "version": "0.9.136", "description": "Budibase Web Server", "main": "src/index.js", "repository": { @@ -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", @@ -64,9 +62,9 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.125-alpha.18", - "@budibase/client": "^0.9.125-alpha.18", - "@budibase/string-templates": "^0.9.125-alpha.18", + "@budibase/auth": "^0.9.136", + "@budibase/client": "^0.9.136", + "@budibase/string-templates": "^0.9.136", "@elastic/elasticsearch": "7.10.0", "@koa/router": "8.0.0", "@sendgrid/mail": "7.1.1", @@ -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", @@ -112,12 +110,14 @@ "to-json-schema": "0.2.5", "uuid": "3.3.2", "validate.js": "0.13.1", + "worker-farm": "1.7.0", "yargs": "13.2.4", "zlib": "1.0.5" }, "devDependencies": { "@babel/core": "^7.14.3", "@babel/preset-env": "^7.14.4", + "@budibase/standard-components": "^0.9.136", "@jest/test-sequencer": "^24.8.0", "@types/bull": "^3.15.1", "@types/jest": "^26.0.23", From cf524423fbddd825a5fef5797968145fdb33df29 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 21 Sep 2021 20:28:33 +0100 Subject: [PATCH 3/9] JSdoc --- .../builder/src/analytics/IntercomClient.js | 26 +++++++++++++++++-- .../builder/src/analytics/PosthogClient.js | 26 ++++++++++++++----- .../builder/src/analytics/SentryClient.js | 8 ++++++ 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/packages/builder/src/analytics/IntercomClient.js b/packages/builder/src/analytics/IntercomClient.js index 10c2f99695..8cc7e35bbf 100644 --- a/packages/builder/src/analytics/IntercomClient.js +++ b/packages/builder/src/analytics/IntercomClient.js @@ -3,6 +3,9 @@ export default class IntercomClient { this.token = token } + /** + * Instantiate intercom using their provided script. + */ init() { if (!this.token) return @@ -43,6 +46,11 @@ export default class IntercomClient { } } + /** + * Show the intercom chat bubble. + * @param {Object} user - user to identify + * @returns Intercom global object + */ show(user = {}) { if (!this.initialised) return @@ -52,21 +60,35 @@ export default class IntercomClient { }) } + /** + * 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 - window.Intercom("trackEvent", event, props) + return window.Intercom("trackEvent", event, props) } + /** + * Disassociate the user from the current session. + * @returns Intercom global object + */ logout() { if (!this.initialised) return - window.Intercom("shutdown") + return window.Intercom("shutdown") } } diff --git a/packages/builder/src/analytics/PosthogClient.js b/packages/builder/src/analytics/PosthogClient.js index 8387c310d3..4c45c10216 100644 --- a/packages/builder/src/analytics/PosthogClient.js +++ b/packages/builder/src/analytics/PosthogClient.js @@ -20,24 +20,31 @@ export default class PosthogClient { 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) } - captureException(err) { - if (!this.initialised) return - - this.captureEvent("Error", { error: err.message ? err.message : err }) - } - + /** + * 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 @@ -45,6 +52,10 @@ export default class PosthogClient { posthog.capture(eventName, props) } + /** + * Submit NPS feedback to posthog. + * @param {Object} values - NPS Values + */ npsFeedback(values) { if (!this.initialised) return @@ -58,6 +69,9 @@ export default class PosthogClient { posthog.capture(Events.NPS.SUBMITTED, prefixedFeedback) } + /** + * Reset posthog user back to initial state on logout. + */ logout() { if (!this.initialised) return diff --git a/packages/builder/src/analytics/SentryClient.js b/packages/builder/src/analytics/SentryClient.js index b871ea15d9..25e4c37690 100644 --- a/packages/builder/src/analytics/SentryClient.js +++ b/packages/builder/src/analytics/SentryClient.js @@ -13,12 +13,20 @@ export default class SentryClient { } } + /** + * 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 From 5ba94ff705fa2f5fe71836cefecab4a7b48c87a4 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 21 Sep 2021 20:38:53 +0100 Subject: [PATCH 4/9] remove superfluous deps --- packages/server/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index fb2e522b87..8c967f8bf7 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -110,14 +110,12 @@ "to-json-schema": "0.2.5", "uuid": "3.3.2", "validate.js": "0.13.1", - "worker-farm": "1.7.0", "yargs": "13.2.4", "zlib": "1.0.5" }, "devDependencies": { "@babel/core": "^7.14.3", "@babel/preset-env": "^7.14.4", - "@budibase/standard-components": "^0.9.136", "@jest/test-sequencer": "^24.8.0", "@types/bull": "^3.15.1", "@types/jest": "^26.0.23", From da1b6692eb75b041906fae5ab271193f906fbd23 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 21 Sep 2021 20:39:56 +0100 Subject: [PATCH 5/9] turn analytics off for self hosted --- packages/server/src/api/controllers/analytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", } } From e1a6f27d70262ebda2426a71ddc8284f1cd6c2b3 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Tue, 21 Sep 2021 20:47:04 +0100 Subject: [PATCH 6/9] lint --- packages/builder/src/analytics/PosthogClient.js | 4 ++-- packages/builder/src/analytics/SentryClient.js | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/builder/src/analytics/PosthogClient.js b/packages/builder/src/analytics/PosthogClient.js index 4c45c10216..0a1fde42ea 100644 --- a/packages/builder/src/analytics/PosthogClient.js +++ b/packages/builder/src/analytics/PosthogClient.js @@ -21,7 +21,7 @@ export default class PosthogClient { } /** - * Set the posthog context to the current user + * Set the posthog context to the current user * @param {String} id - unique user id */ identify(id) { @@ -70,7 +70,7 @@ export default class PosthogClient { } /** - * Reset posthog user back to initial state on logout. + * Reset posthog user back to initial state on logout. */ logout() { if (!this.initialised) return diff --git a/packages/builder/src/analytics/SentryClient.js b/packages/builder/src/analytics/SentryClient.js index 25e4c37690..2a1f8732e3 100644 --- a/packages/builder/src/analytics/SentryClient.js +++ b/packages/builder/src/analytics/SentryClient.js @@ -13,20 +13,20 @@ export default class SentryClient { } } - /** - * Capture an exception and send it to sentry. - * @param {Error} err - JS error object - */ + /** + * 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 user in sentry. + * @param {String} id - Unique user id + */ identify(id) { if (!this.initalised) return From 87b61b5c549032ab974917cb4bc79873b0c65b1f Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 22 Sep 2021 09:29:51 +0100 Subject: [PATCH 7/9] added user created at timestamp --- .../builder/portal/manage/users/_components/AddUserModal.svelte | 2 +- packages/worker/src/api/controllers/global/users.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 be5d6db746..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 @@ -26,7 +26,7 @@ notifications.error(res.message) } else { notifications.success(res.message) - analytics.captureEvent(Events.USER.INVITE) + analytics.captureEvent(Events.USER.INVITE, { type: selected }) } } diff --git a/packages/worker/src/api/controllers/global/users.js b/packages/worker/src/api/controllers/global/users.js index 8f754e2922..ac92f3902c 100644 --- a/packages/worker/src/api/controllers/global/users.js +++ b/packages/worker/src/api/controllers/global/users.js @@ -63,6 +63,7 @@ async function saveUser(user, tenantId, hashPassword = true) { _id = _id || generateGlobalUserID() user = { + createdAt: Date.now(), ...dbUser, ...user, _id, @@ -138,6 +139,7 @@ exports.adminUser = async ctx => { const user = { email: email, password: password, + createdAt: Date.now(), roles: {}, builder: { global: true, From 2523d5464184ae4117980ecba3a2af3690482aa9 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 22 Sep 2021 09:38:32 +0100 Subject: [PATCH 8/9] account for user size in intercom --- packages/builder/src/stores/portal/auth.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index 22f7fedca4..cb4e14c4a9 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -54,13 +54,15 @@ export function createAuthStore() { if (user) { analytics.activate().then(() => { analytics.identify(user._id, user) - // TODO: check if the user is a certain org size - // analytics.showChat({ - // email: user.email, - // name: user.name, - // user_id: user._id, - // tenant: user.tenantId - // }) + 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 + }) + } }) } } From 24837ef2f1c6c47f13d19c99cdf4e18c3e6756f0 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Wed, 22 Sep 2021 12:26:02 +0100 Subject: [PATCH 9/9] lint --- packages/builder/src/stores/portal/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/builder/src/stores/portal/auth.js b/packages/builder/src/stores/portal/auth.js index cb4e14c4a9..e33a1f22ac 100644 --- a/packages/builder/src/stores/portal/auth.js +++ b/packages/builder/src/stores/portal/auth.js @@ -60,7 +60,7 @@ export function createAuthStore() { created_at: user.createdAt || Date.now(), name: user.name, user_id: user._id, - tenant: user.tenantId + tenant: user.tenantId, }) } })