forked from tsai/budibase
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