mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
34 changed files with 408 additions and 208 deletions
@ -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), |
|||
} |
|||
@ -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") |
|||
} |
|||
} |
|||
@ -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() |
|||
} |
|||
} |
|||
@ -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 }) |
|||
}) |
|||
} |
|||
} |
|||
@ -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", |
|||
}, |
|||
} |
|||
@ -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 |
|||
Loading…
Reference in new issue