analytics clients

This commit is contained in:
Martin McKeaveney 2021-09-21 11:47:14 +01:00
parent f5edc45570
commit 184061a56b
19 changed files with 412 additions and 150 deletions

View File

@ -7,6 +7,7 @@ on:
env: env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}

View File

@ -7,6 +7,7 @@ on:
env: env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }}

View File

@ -73,6 +73,7 @@ exports.ObjectStore = bucket => {
AWS.config.update({ AWS.config.update({
accessKeyId: env.MINIO_ACCESS_KEY, accessKeyId: env.MINIO_ACCESS_KEY,
secretAccessKey: env.MINIO_SECRET_KEY, secretAccessKey: env.MINIO_SECRET_KEY,
region: env.AWS_REGION
}) })
const config = { const config = {
s3ForcePathStyle: true, s3ForcePathStyle: true,

View File

@ -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),
}

View File

@ -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);
}
}

View File

@ -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),
// }

View File

@ -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 })
// }

View File

@ -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"
}
}

View File

@ -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

View File

@ -3,7 +3,7 @@ import { getAutomationStore } from "./store/automation"
import { getHostingStore } from "./store/hosting" import { getHostingStore } from "./store/hosting"
import { getThemeStore } from "./store/theme" import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store" import { derived, writable } from "svelte/store"
import analytics from "analytics" import analytics, { Events } from "analytics"
import { FrontendTypes, LAYOUT_NAMES } from "../constants" import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { findComponent } from "./storeUtils" import { findComponent } from "./storeUtils"
@ -58,7 +58,7 @@ export const selectedAccessRole = writable("BASIC")
export const initialise = async () => { export const initialise = async () => {
try { try {
await analytics.activate() await analytics.activate()
analytics.captureEvent("Builder Started") analytics.captureEvent(Events.BUILDER.STARTED)
} catch (err) { } catch (err) {
console.log(err) console.log(err)
} }

View File

@ -2,7 +2,8 @@
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { Button, Modal, notifications, ModalContent } from "@budibase/bbui" import { Button, Modal, notifications, ModalContent } from "@budibase/bbui"
import api from "builderStore/api" import api from "builderStore/api"
import analytics from "analytics" import analytics, { Events } from "analytics"
import { store } from "builderStore"
const DeploymentStatus = { const DeploymentStatus = {
SUCCESS: "SUCCESS", SUCCESS: "SUCCESS",
@ -23,6 +24,9 @@
if (response.status !== 200) { if (response.status !== 200) {
throw new Error(`status ${response.status}`) throw new Error(`status ${response.status}`)
} else { } else {
analytics.captureEvent(Events.APP.PUBLISHED, {
appId: $store.appId
})
notifications.success(`Application published successfully`) notifications.success(`Application published successfully`)
} }
} catch (err) { } catch (err) {

View File

@ -12,7 +12,7 @@
import { admin } from "stores/portal" import { admin } from "stores/portal"
import { string, mixed, object } from "yup" import { string, mixed, object } from "yup"
import api, { get, post } from "builderStore/api" import api, { get, post } from "builderStore/api"
import analytics from "analytics" import analytics, { Events } from "analytics"
import { onMount } from "svelte" import { onMount } from "svelte"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
@ -98,9 +98,9 @@
throw new Error(appJson.message) throw new Error(appJson.message)
} }
analytics.captureEvent("App Created", { analytics.captureEvent(Events.APP.CREATED, {
name: $values.name, name: $values.name,
appId: appJson._id, appId: appJson.instance._id,
template, template,
}) })

View File

@ -15,6 +15,7 @@
import OIDCButton from "./_components/OIDCButton.svelte" import OIDCButton from "./_components/OIDCButton.svelte"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { onMount } from "svelte" import { onMount } from "svelte"
import analytics from "analytics"
let username = "" let username = ""
let password = "" let password = ""
@ -25,10 +26,20 @@
async function login() { async function login() {
try { try {
await auth.login({ const { user } = await auth.login({
username, username,
password, 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) { if ($auth?.user?.forceResetPassword) {
$goto("./reset") $goto("./reset")
} else { } else {

View File

@ -69,9 +69,9 @@
const checkKeys = async () => { const checkKeys = async () => {
const response = await api.get(`/api/keys/`) const response = await api.get(`/api/keys/`)
const keys = await response.json() const keys = await response.json()
if (keys.userId) { // if (keys.userId) {
analytics.identify(keys.userId) // analytics.identify(keys.userId)
} // }
} }
const initiateAppCreation = () => { const initiateAppCreation = () => {

View File

@ -23,6 +23,7 @@
import api from "builderStore/api" import api from "builderStore/api"
import { organisation, auth, admin } from "stores/portal" import { organisation, auth, admin } from "stores/portal"
import { uuid } from "builderStore/uuid" import { uuid } from "builderStore/uuid"
import analytics, { Events } from "analytics"
$: tenantId = $auth.tenantId $: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy $: multiTenancyEnabled = $admin.multiTenancy
@ -209,6 +210,7 @@
providers[res.type]._id = res._id providers[res.type]._id = res._id
}) })
notifications.success(`Settings saved.`) notifications.success(`Settings saved.`)
analytics.captureEvent(Events.SSO.SAVED)
}) })
.catch(err => { .catch(err => {
notifications.error(`Failed to update auth settings. ${err}`) notifications.error(`Failed to update auth settings. ${err}`)

View File

@ -16,6 +16,7 @@
import { email } from "stores/portal" import { email } from "stores/portal"
import api from "builderStore/api" import api from "builderStore/api"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import analytics, { Events } from "analytics"
const ConfigTypes = { const ConfigTypes = {
SMTP: "smtp", SMTP: "smtp",
@ -69,6 +70,7 @@
smtpConfig._rev = json._rev smtpConfig._rev = json._rev
smtpConfig._id = json._id smtpConfig._id = json._id
notifications.success(`Settings saved.`) notifications.success(`Settings saved.`)
analytics.captureEvent(Events.SMTP.SAVED)
} }
} }

View File

@ -10,6 +10,7 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { createValidationStore, emailValidator } from "helpers/validation" import { createValidationStore, emailValidator } from "helpers/validation"
import { users } from "stores/portal" import { users } from "stores/portal"
import analytics, { Events } from "analytics"
export let disabled export let disabled
@ -25,6 +26,7 @@
notifications.error(res.message) notifications.error(res.message)
} else { } else {
notifications.success(res.message) notifications.success(res.message)
analytics.captureEvent(Events.USER.INVITE)
} }
} }
</script> </script>

View File

@ -25,7 +25,7 @@
} }
const values = writable({ const values = writable({
analytics: !analytics.disabled(), analytics: analytics.enabled,
company: $organisation.company, company: $organisation.company,
platformUrl: $organisation.platformUrl, platformUrl: $organisation.platformUrl,
logo: $organisation.logoUrl logo: $organisation.logoUrl

View File

@ -22,6 +22,9 @@ export default ({ mode }) => {
isProduction ? "production" : "development" isProduction ? "production" : "development"
), ),
"process.env.POSTHOG_TOKEN": JSON.stringify(process.env.POSTHOG_TOKEN), "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.POSTHOG_URL": JSON.stringify(process.env.POSTHOG_URL),
"process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN), "process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN),
}), }),