diff --git a/.env.example b/.env.example
index b41817769..fab8d26ec 100644
--- a/.env.example
+++ b/.env.example
@@ -13,6 +13,7 @@ STORAGE_BUCKET=postwoman-api.appspot.com
MESSAGING_SENDER_ID=421993993223
APP_ID=1:421993993223:web:ec0baa8ee8c02ffa1fc6a2
MEASUREMENT_ID=G-ERJ6025CEB
+FB_MEASUREMENT_ID=G-BBJ3R80PJT
# Base URL
BASE_URL=https://hoppscotch.io
diff --git a/components/realtime/Mqtt.vue b/components/realtime/Mqtt.vue
index ecce3de2f..afec01f2b 100644
--- a/components/realtime/Mqtt.vue
+++ b/components/realtime/Mqtt.vue
@@ -131,6 +131,7 @@
import { Splitpanes, Pane } from "splitpanes"
import Paho from "paho-mqtt"
import debounce from "~/helpers/utils/debounce"
+import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
export default {
components: { Splitpanes, Pane },
@@ -202,6 +203,10 @@ export default {
})
this.client.onConnectionLost = this.onConnectionLost
this.client.onMessageArrived = this.onMessageArrived
+
+ logHoppRequestRunToAnalytics({
+ platform: "mqtt",
+ })
},
onConnectionFailure() {
this.connectionState = false
diff --git a/components/realtime/Socketio.vue b/components/realtime/Socketio.vue
index d1cfd05ef..ca0d9314d 100644
--- a/components/realtime/Socketio.vue
+++ b/components/realtime/Socketio.vue
@@ -153,6 +153,7 @@ import { Splitpanes, Pane } from "splitpanes"
import { io as Client } from "socket.io-client"
import wildcard from "socketio-wildcard"
import debounce from "~/helpers/utils/debounce"
+import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
export default {
components: { Splitpanes, Pane },
@@ -275,6 +276,10 @@ export default {
icon: "error",
})
}
+
+ logHoppRequestRunToAnalytics({
+ platform: "socketio",
+ })
},
disconnect() {
this.io.close()
diff --git a/components/realtime/Sse.vue b/components/realtime/Sse.vue
index 941ef7a80..ed6f39a3d 100644
--- a/components/realtime/Sse.vue
+++ b/components/realtime/Sse.vue
@@ -53,6 +53,7 @@
diff --git a/components/smart/ColorModePicker.vue b/components/smart/ColorModePicker.vue
index 9b2fdcacb..3bf62ebb9 100644
--- a/components/smart/ColorModePicker.vue
+++ b/components/smart/ColorModePicker.vue
@@ -15,15 +15,31 @@
-
diff --git a/helpers/fb/analytics.ts b/helpers/fb/analytics.ts
new file mode 100644
index 000000000..27d0aa4f4
--- /dev/null
+++ b/helpers/fb/analytics.ts
@@ -0,0 +1,93 @@
+import firebase from "firebase"
+import { authEvents$ } from "./auth"
+import {
+ HoppAccentColor,
+ HoppBgColor,
+ settings$,
+ settingsStore,
+} from "~/newstore/settings"
+
+let analytics: firebase.analytics.Analytics
+
+type SettingsCustomDimensions = {
+ usesProxy: boolean
+ usesExtension: boolean
+ usesScrollInto: boolean
+ syncCollections: boolean
+ syncEnvironments: boolean
+ syncHistory: boolean
+ usesBg: HoppBgColor
+ usesAccent: HoppAccentColor
+ usesTelemetry: boolean
+}
+
+type HoppRequestEvent =
+ | {
+ platform: "rest" | "graphql-query" | "graphql-schema"
+ strategy: "normal" | "proxy" | "extension"
+ }
+ | { platform: "wss" | "sse" | "socketio" | "mqtt" }
+
+export function initAnalytics() {
+ analytics = firebase.app().analytics()
+
+ initLoginListeners()
+ initSettingsListeners()
+}
+
+function initLoginListeners() {
+ authEvents$.subscribe((ev) => {
+ if (ev.event === "login") {
+ if (settingsStore.value.TELEMETRY_ENABLED) {
+ analytics.setUserId(ev.user.uid)
+
+ analytics.logEvent("login", {
+ method: ev.user.providerData[0]?.providerId, // Assume the first provider is the login provider
+ })
+ }
+ } else if (ev.event === "logout") {
+ if (settingsStore.value.TELEMETRY_ENABLED) {
+ analytics.logEvent("logout")
+ }
+ }
+ })
+}
+
+function initSettingsListeners() {
+ // Keep track of the telemetry status
+ let telemetryStatus = settingsStore.value.TELEMETRY_ENABLED
+
+ settings$.subscribe((settings) => {
+ const conf: SettingsCustomDimensions = {
+ usesProxy: settings.PROXY_ENABLED,
+ usesExtension: settings.EXTENSIONS_ENABLED,
+ usesScrollInto: settings.SCROLL_INTO_ENABLED,
+ syncCollections: settings.syncCollections,
+ syncEnvironments: settings.syncEnvironments,
+ syncHistory: settings.syncHistory,
+ usesAccent: settings.THEME_COLOR,
+ usesBg: settings.BG_COLOR,
+ usesTelemetry: settings.TELEMETRY_ENABLED,
+ }
+
+ // User toggled telemetry mode to off or to on
+ if (
+ (telemetryStatus && !settings.TELEMETRY_ENABLED) ||
+ settings.TELEMETRY_ENABLED
+ ) {
+ analytics.setUserProperties(conf)
+ }
+
+ telemetryStatus = settings.TELEMETRY_ENABLED
+
+ analytics.setAnalyticsCollectionEnabled(telemetryStatus)
+ })
+
+ analytics.setAnalyticsCollectionEnabled(telemetryStatus)
+}
+
+export function logHoppRequestRunToAnalytics(ev: HoppRequestEvent) {
+ if (settingsStore.value.TELEMETRY_ENABLED) {
+ analytics.logEvent("hopp-request", ev)
+ }
+}
diff --git a/helpers/fb/auth.ts b/helpers/fb/auth.ts
index 5fa323a45..08a7c0874 100644
--- a/helpers/fb/auth.ts
+++ b/helpers/fb/auth.ts
@@ -1,11 +1,16 @@
import firebase from "firebase"
-import { BehaviorSubject } from "rxjs"
+import { BehaviorSubject, Subject } from "rxjs"
export type HoppUser = firebase.User & {
provider?: string
accessToken?: string
}
+type AuthEvents =
+ | { event: "login"; user: HoppUser }
+ | { event: "logout" }
+ | { event: "authTokenUpdate"; user: HoppUser; newToken: string | null }
+
/**
* A BehaviorSubject emitting the currently logged in user (or null if not logged in)
*/
@@ -15,6 +20,11 @@ export const currentUser$ = new BehaviorSubject(null)
*/
export const authIdToken$ = new BehaviorSubject(null)
+/**
+ * A subject that emits events related to authentication flows
+ */
+export const authEvents$ = new Subject()
+
/**
* Initializes the firebase authentication related subjects
*/
@@ -22,6 +32,9 @@ export function initAuth() {
let extraSnapshotStop: (() => void) | null = null
firebase.auth().onAuthStateChanged((user) => {
+ /** Whether the user was logged in before */
+ const wasLoggedIn = currentUser$.value !== null
+
if (!user && extraSnapshotStop) {
extraSnapshotStop()
extraSnapshotStop = null
@@ -61,14 +74,35 @@ export function initAuth() {
userUpdate.provider = data.provider
userUpdate.accessToken = data.accessToken
}
+
+ currentUser$.next(userUpdate)
})
}
currentUser$.next(user)
+
+ // User wasn't found before, but now is there (login happened)
+ if (!wasLoggedIn && user) {
+ authEvents$.next({
+ event: "login",
+ user: currentUser$.value!!,
+ })
+ } else if (wasLoggedIn && !user) {
+ // User was found before, but now is not there (logout happened)
+ authEvents$.next({
+ event: "logout",
+ })
+ }
})
firebase.auth().onIdTokenChanged(async (user) => {
if (user) {
authIdToken$.next(await user.getIdToken())
+
+ authEvents$.next({
+ event: "authTokenUpdate",
+ newToken: authIdToken$.value,
+ user: currentUser$.value!!, // Force not-null because user is defined
+ })
} else {
authIdToken$.next(null)
}
diff --git a/helpers/fb/index.ts b/helpers/fb/index.ts
index 25566d530..7b90baa17 100644
--- a/helpers/fb/index.ts
+++ b/helpers/fb/index.ts
@@ -1,4 +1,5 @@
import firebase from "firebase"
+import { initAnalytics } from "./analytics"
import { initAuth } from "./auth"
import { initCollections } from "./collections"
import { initEnvironments } from "./environments"
@@ -13,21 +14,27 @@ const firebaseConfig = {
storageBucket: process.env.STORAGE_BUCKET,
messagingSenderId: process.env.MESSAGING_SENDER_ID,
appId: process.env.APP_ID,
- measurementId: process.env.MEASUREMENT_ID,
+ measurementId: process.env.FB_MEASUREMENT_ID,
}
let initialized = false
export function initializeFirebase() {
if (!initialized) {
- firebase.initializeApp(firebaseConfig)
+ try {
+ firebase.initializeApp(firebaseConfig)
- initAuth()
- initSettings()
- initCollections()
- initHistory()
- initEnvironments()
+ initAuth()
+ initSettings()
+ initCollections()
+ initHistory()
+ initEnvironments()
+ initAnalytics()
- initialized = true
+ initialized = true
+ } catch (e) {
+ // initializeApp throws exception if we reinitialize
+ initialized = true
+ }
}
}
diff --git a/helpers/network.js b/helpers/network.js
index 4ba209b03..ca291440e 100644
--- a/helpers/network.js
+++ b/helpers/network.js
@@ -25,5 +25,21 @@ const runAppropriateStrategy = (req) => {
return AxiosStrategy(req)
}
+/**
+ * Returns an identifier for how a request will be ran
+ * if the system is asked to fire a request
+ *
+ * @returns {"normal" | "extension" | "proxy"}
+ */
+export function getCurrentStrategyID() {
+ if (isExtensionsAllowed() && hasExtensionInstalled()) {
+ return "extension"
+ } else if (settingsStore.value.PROXY_ENABLED) {
+ return "proxy"
+ } else {
+ return "normal"
+ }
+}
+
export const sendNetworkRequest = (req) =>
runAppropriateStrategy(req).finally(() => window.$nuxt.$loading.finish())
diff --git a/lang/en-US.json b/lang/en-US.json
index 925aa778c..5f03d26a9 100644
--- a/lang/en-US.json
+++ b/lang/en-US.json
@@ -286,6 +286,8 @@
"are_you_sure_remove_folder": "Are you sure you want to remove this folder?",
"are_you_sure_remove_request": "Are you sure you want to remove this request?",
"are_you_sure_remove_environment": "Are you sure you want to remove this environment?",
+ "are_you_sure_remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
+ "telemetry_helps_us": "Telemetry helps us to personalize our operations and deliver the best experience to you.",
"select_next_method": "Select Next method",
"select_previous_method": "Select Previous method",
"select_get_method": "Select GET method",
@@ -345,5 +347,6 @@
"share": "Share",
"interceptor": "Interceptor",
"profile": "Profile",
- "are_you_sure_logout": "Are you sure you want to logout?"
+ "are_you_sure_logout": "Are you sure you want to logout?",
+ "telemetry": "Telemetry"
}
diff --git a/layouts/default.vue b/layouts/default.vue
index 70f9d3503..0172464c3 100644
--- a/layouts/default.vue
+++ b/layouts/default.vue
@@ -66,15 +66,12 @@