From b5fc2f6a709e9adbd554dff958f2c86c362b764e Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 25 Mar 2025 22:26:10 +0800 Subject: [PATCH] feat: add some tracker Signed-off-by: Innei --- .../FeedFoundCanBeFollowErrorFallback.tsx | 1 - .../renderer/src/hooks/biz/useFeedActions.tsx | 4 +-- .../src/hooks/biz/useFeedFormModal.tsx | 4 +-- .../src/renderer/src/hooks/biz/useFollow.tsx | 2 -- .../src/renderer/src/initialize/helper.ts | 12 ++----- .../src/modules/command/commands/list.tsx | 2 +- .../src/modules/discover/DiscoverFeedForm.tsx | 1 - .../src/modules/discover/feed-form.tsx | 30 ++++++----------- .../src/modules/discover/list-form.tsx | 30 +++++------------ .../new-user-guide/guide-modal-content.tsx | 9 +++++- .../src/renderer/src/modules/panel/cmdn.tsx | 1 - .../src/renderer/src/pages/add/index.tsx | 32 ------------------- apps/mobile/package.json | 1 + apps/mobile/src/initialize/analytics.ts | 19 +++++++++++ apps/mobile/src/initialize/index.ts | 20 ++++++++++-- apps/mobile/src/lib/op.ts | 11 +++++-- .../entry-list/templates/EntryNormalItem.tsx | 6 ++++ .../entry-list/templates/EntryPictureItem.tsx | 5 +++ .../entry-list/templates/EntrySocialItem.tsx | 7 +++- .../entry-list/templates/EntryVideoItem.tsx | 6 ++++ apps/mobile/src/screens/(modal)/sign-up.tsx | 2 ++ apps/mobile/src/screens/onboarding.tsx | 3 ++ apps/mobile/src/store/subscription/store.ts | 4 ++- apps/mobile/src/store/user/hooks.ts | 16 ++-------- apps/ssr/client/initialize/analytics.ts | 22 ++----------- apps/ssr/client/initialize/helper.ts | 14 +++----- apps/ssr/client/pages/(login)/register.tsx | 4 +++ apps/ssr/package.json | 1 + packages/tracker/src/index.ts | 26 +++++++++++++++ packages/tracker/src/op/api.ts | 3 -- packages/tracker/src/op/index.ts | 2 ++ packages/tracker/src/points.ts | 32 +++++++++++++++++++ pnpm-lock.yaml | 6 ++++ 33 files changed, 189 insertions(+), 149 deletions(-) delete mode 100644 apps/desktop/src/renderer/src/pages/add/index.tsx create mode 100644 apps/mobile/src/initialize/analytics.ts diff --git a/apps/desktop/src/renderer/src/components/errors/FeedFoundCanBeFollowErrorFallback.tsx b/apps/desktop/src/renderer/src/components/errors/FeedFoundCanBeFollowErrorFallback.tsx index eb1361097..4089267e7 100644 --- a/apps/desktop/src/renderer/src/components/errors/FeedFoundCanBeFollowErrorFallback.tsx +++ b/apps/desktop/src/renderer/src/components/errors/FeedFoundCanBeFollowErrorFallback.tsx @@ -49,7 +49,6 @@ const FeedFoundCanBeFollowErrorFallback: FC = ({ resetErr title: t("feed_form.add_feed"), content: ({ dismiss }) => ( { present({ title: t("sidebar.feed_actions.edit_feed"), - content: ({ dismiss }) => , + content: ({ dismiss }) => , }) }, }, @@ -424,7 +424,7 @@ export const useListActions = ({ listId, view }: { listId: string; view?: FeedVi click: () => { present({ title: t("sidebar.feed_actions.edit_list"), - content: ({ dismiss }) => , + content: ({ dismiss }) => , }) }, }, diff --git a/apps/desktop/src/renderer/src/hooks/biz/useFeedFormModal.tsx b/apps/desktop/src/renderer/src/hooks/biz/useFeedFormModal.tsx index 5d2a6c5d9..3840d51b9 100644 --- a/apps/desktop/src/renderer/src/hooks/biz/useFeedFormModal.tsx +++ b/apps/desktop/src/renderer/src/hooks/biz/useFeedFormModal.tsx @@ -28,9 +28,9 @@ export const usePresentFeedFormModal = () => { title: isList ? t("sidebar.feed_actions.edit_list") : t("sidebar.feed_actions.edit_feed"), content: ({ dismiss }) => isList ? ( - + ) : ( - + ), }) } else { diff --git a/apps/desktop/src/renderer/src/hooks/biz/useFollow.tsx b/apps/desktop/src/renderer/src/hooks/biz/useFollow.tsx index 9c2751902..e63aee0fa 100644 --- a/apps/desktop/src/renderer/src/hooks/biz/useFollow.tsx +++ b/apps/desktop/src/renderer/src/hooks/biz/useFollow.tsx @@ -87,14 +87,12 @@ export const useFollow = () => { } return options?.isList ? ( ) : ( { - op.identify({ - profileId: user.id, - email: user.email, - avatar: user.image ?? undefined, - lastName: user.name ?? undefined, - properties: { - handle: user.handle, - name: user.name, - }, - }) + identifyUserOpenPanel(user, op.identify.bind(op)) op.track("identify", { user_id: user.id, }) diff --git a/apps/desktop/src/renderer/src/modules/command/commands/list.tsx b/apps/desktop/src/renderer/src/modules/command/commands/list.tsx index 7c74ca754..0d5c93034 100644 --- a/apps/desktop/src/renderer/src/modules/command/commands/list.tsx +++ b/apps/desktop/src/renderer/src/modules/command/commands/list.tsx @@ -28,7 +28,7 @@ export const useRegisterListCommands = () => { if (!listId) return present({ title: t("sidebar.feed_actions.edit_list"), - content: ({ dismiss }) => , + content: ({ dismiss }) => , }) }, }, diff --git a/apps/desktop/src/renderer/src/modules/discover/DiscoverFeedForm.tsx b/apps/desktop/src/renderer/src/modules/discover/DiscoverFeedForm.tsx index 69c4a7fd9..a85e7689b 100644 --- a/apps/desktop/src/renderer/src/modules/discover/DiscoverFeedForm.tsx +++ b/apps/desktop/src/renderer/src/modules/discover/DiscoverFeedForm.tsx @@ -203,7 +203,6 @@ export const DiscoverFeedForm = ({ title: t("feed_form.add_feed"), content: () => ( void -}> = ({ id: _id, defaultValues = defaultValue, url, asWidget, onSuccess }) => { +}> = ({ id: _id, defaultValues = defaultValue, url, onSuccess }) => { const queryParams = { id: _id, url } const feedQuery = useFeed(queryParams) @@ -67,34 +67,22 @@ export const FeedForm: Component<{ url, }) as FeedModel - const hasSub = useSubscriptionByFeedId(feed?.id || "") - const isSubscribed = !!feedQuery.data?.subscription || hasSub - const { t } = useTranslation() return (
- {!asWidget && ( -
- - {isSubscribed ? t("feed_form.update_follow") : t("feed_form.add_follow")} -
- )} - {feed ? ( id?: string - asWidget?: boolean + onSuccess?: () => void subscriptionData?: { view?: number @@ -244,8 +232,8 @@ const FeedInnerForm = ({ duration: 1000, }) - if (!asWidget && !isSubscribed) { - window.close() + if (!isSubscribed) { + tracker.subscribe({ feedId: feed.id, view: Number.parseInt(variables.view) }) } onSuccess?.() diff --git a/apps/desktop/src/renderer/src/modules/discover/list-form.tsx b/apps/desktop/src/renderer/src/modules/discover/list-form.tsx index 25c55d631..95f267d31 100644 --- a/apps/desktop/src/renderer/src/modules/discover/list-form.tsx +++ b/apps/desktop/src/renderer/src/modules/discover/list-form.tsx @@ -1,4 +1,3 @@ -import { Logo } from "@follow/components/icons/logo.jsx" import { Button } from "@follow/components/ui/button/index.js" import { Card, CardHeader } from "@follow/components/ui/card/index.jsx" import { @@ -15,6 +14,7 @@ import { LoadingCircle } from "@follow/components/ui/loading/index.jsx" import { Switch } from "@follow/components/ui/switch/index.jsx" import { FeedViewType } from "@follow/constants" import type { ListModel } from "@follow/models/types" +import { tracker } from "@follow/tracker" import { cn } from "@follow/utils/utils" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" @@ -55,10 +55,8 @@ export const ListForm: Component<{ defaultValues?: ListFormDataValuesType - asWidget?: boolean - onSuccess?: () => void -}> = ({ id: _id, defaultValues = defaultValue, asWidget, onSuccess }) => { +}> = ({ id: _id, defaultValues = defaultValue, onSuccess }) => { const queryParams = { id: _id } const feedQuery = useList(queryParams) @@ -66,33 +64,21 @@ export const ListForm: Component<{ const id = feedQuery.data?.list.id || _id const list = useListById(id) - const hasSub = useSubscriptionByFeedId(list?.id || "") - const isSubscribed = !!feedQuery.data?.subscription || hasSub - const { t } = useTranslation() return (
- {!asWidget && ( -
- - {isSubscribed ? t("feed_form.update_follow") : t("feed_form.add_follow")} -
- )} - {list ? ( id?: string - asWidget?: boolean + onSuccess?: () => void subscriptionData?: { view?: number @@ -241,8 +227,8 @@ const ListInnerForm = ({ duration: 1000, }) - if (!asWidget && !isSubscribed) { - window.close() + if (!isSubscribed) { + tracker.subscribe({ listId: list.id, view: Number.parseInt(variables.view) }) } onSuccess?.() diff --git a/apps/desktop/src/renderer/src/modules/new-user-guide/guide-modal-content.tsx b/apps/desktop/src/renderer/src/modules/new-user-guide/guide-modal-content.tsx index ef712cb56..b96a27b94 100644 --- a/apps/desktop/src/renderer/src/modules/new-user-guide/guide-modal-content.tsx +++ b/apps/desktop/src/renderer/src/modules/new-user-guide/guide-modal-content.tsx @@ -1,10 +1,11 @@ import { Logo } from "@follow/components/icons/logo.jsx" import { Button } from "@follow/components/ui/button/index.js" import { Kbd } from "@follow/components/ui/kbd/Kbd.js" +import { tracker } from "@follow/tracker" import { cn } from "@follow/utils/utils" import { AnimatePresence, m } from "framer-motion" import type { ComponentProps, FunctionComponentElement } from "react" -import { createElement, useCallback, useMemo, useState } from "react" +import { createElement, useCallback, useEffect, useMemo, useState } from "react" import { Trans, useTranslation } from "react-i18next" import { useGeneralSettingKey } from "~/atoms/settings/general" @@ -156,6 +157,12 @@ export function GuideModalContent({ onClose }: { onClose: () => void }) { [step, totalSteps], ) + useEffect(() => { + tracker.onBoarding({ + step, + done: status === "complete", + }) + }, [status, step]) const title = useMemo(() => guideSteps[step - 1]?.title, [guideSteps, step]) const [isLottieAnimating, setIsLottieAnimating] = useState(false) diff --git a/apps/desktop/src/renderer/src/modules/panel/cmdn.tsx b/apps/desktop/src/renderer/src/modules/panel/cmdn.tsx index c8eedce2e..1a9a3200b 100644 --- a/apps/desktop/src/renderer/src/modules/panel/cmdn.tsx +++ b/apps/desktop/src/renderer/src/modules/panel/cmdn.tsx @@ -55,7 +55,6 @@ const CmdNPanel = () => { title: t("feed_form.add_feed"), content: () => ( { - document.body.style.overflow = "hidden" - return () => { - document.body.style.overflow = "" - } - }, []) - return ( -
- -
- ) -} diff --git a/apps/mobile/package.json b/apps/mobile/package.json index edf823f71..0730547de 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -26,6 +26,7 @@ "@follow/hooks": "workspace:*", "@follow/models": "workspace:*", "@follow/shared": "workspace:*", + "@follow/tracker": "workspace:*", "@follow/utils": "workspace:*", "@gorhom/portal": "1.0.14", "@hookform/resolvers": "4.1.3", diff --git a/apps/mobile/src/initialize/analytics.ts b/apps/mobile/src/initialize/analytics.ts new file mode 100644 index 000000000..1ced1b1b7 --- /dev/null +++ b/apps/mobile/src/initialize/analytics.ts @@ -0,0 +1,19 @@ +import { identifyUserOpenPanel, setOpenPanelTracker } from "@follow/tracker" +import { nativeApplicationVersion, nativeBuildVersion } from "expo-application" + +import { op } from "../lib/op" +import { whoami } from "../store/user/getters" + +export const initAnalytics = () => { + setOpenPanelTracker(op.track.bind(op)) + const user = whoami() + if (user) { + identifyUserOpenPanel(user, op.identify.bind(op)) + } + + op.setGlobalProperties({ + build: "rn", + version: nativeApplicationVersion, + buildId: nativeBuildVersion, + }) +} diff --git a/apps/mobile/src/initialize/index.ts b/apps/mobile/src/initialize/index.ts index e7acc06d2..989480d68 100644 --- a/apps/mobile/src/initialize/index.ts +++ b/apps/mobile/src/initialize/index.ts @@ -1,4 +1,8 @@ +import { tracker } from "@follow/tracker" +import { nativeApplicationVersion } from "expo-application" + import { initializeDb } from "../database" +import { initAnalytics } from "./analytics" import { initializeAppCheck } from "./app-check" import { initializeDayjs } from "./dayjs" import { hydrateDatabaseToStore, hydrateQueryClient, hydrateSettings } from "./hydrate" @@ -11,18 +15,30 @@ export const initializeApp = async () => { const now = Date.now() initializeDb() + initAnalytics() + await apm("migrateDatabase", migrateDatabase) initializeDayjs() await apm("hydrateSettings", hydrateSettings) + let dataHydratedTime = Date.now() await apm("hydrateDatabaseToStore", hydrateDatabaseToStore) + + dataHydratedTime = Date.now() - dataHydratedTime await apm("hydrateQueryClient", hydrateQueryClient) await apm("initializeAppCheck", initializeAppCheck) const loadingTime = Date.now() - now - console.log(`Initialize done,`, `${loadingTime}ms`) - + tracker.appInit({ + rn: true, + loading_time: loadingTime, + version: nativeApplicationVersion!, + data_hydrated_time: dataHydratedTime, + electron: false, + using_indexed_db: true, + }) await apm("initializePlayer", initializePlayer) + console.log(`Initialize done,`, `${loadingTime}ms`) } const apm = async (label: string, fn: () => Promise | any) => { diff --git a/apps/mobile/src/lib/op.ts b/apps/mobile/src/lib/op.ts index 0793b284d..e94fbf512 100644 --- a/apps/mobile/src/lib/op.ts +++ b/apps/mobile/src/lib/op.ts @@ -1,10 +1,15 @@ -import { OpenPanel } from "@openpanel/sdk" +import { OpenPanel } from "@follow/tracker/src/op" +import { nativeApplicationVersion, nativeBuildVersion } from "expo-application" import { proxyEnv } from "./proxy-env" export const op = new OpenPanel({ clientId: proxyEnv.OPENPANEL_CLIENT_ID ?? "", - apiUrl: proxyEnv.OPENPANEL_API_URL, - // waitForProfile: true, + headers: { + Host: "app.follow.is", + Origin: "https://app.follow.is", + "User-Agent": `Folo/${nativeApplicationVersion}(${nativeBuildVersion})`, + }, + sdkVersion: "1.0.0", }) diff --git a/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx index 457f0c7c9..63e5d6024 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryNormalItem.tsx @@ -1,4 +1,5 @@ import { FeedViewType } from "@follow/constants" +import { tracker } from "@follow/tracker" import { cn, formatEstimatedMins, formatTimeToSeconds } from "@follow/utils" import { useCallback, useEffect, useMemo, useState } from "react" import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from "react-native" @@ -37,6 +38,11 @@ export function EntryNormalItem({ entryId, extraData }: { entryId: string; extra const isHorizontalScrolling = getHorizontalScrolling() if (entry && !isHorizontalScrolling) { preloadWebViewEntry(entry) + tracker.navigateEntry({ + feedId: entry.feedId!, + entryId: entry.id, + }) + navigation.pushControllerView(EntryDetailScreen, { entryId, view, diff --git a/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx index 9d941bde7..1f7ec482e 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx @@ -1,3 +1,4 @@ +import { tracker } from "@follow/tracker" import { uniqBy } from "es-toolkit/compat" import { useMemo } from "react" import { Text, View } from "react-native" @@ -38,6 +39,10 @@ export function EntryPictureItem({ id }: { id: string }) { if (!feed) { return } + tracker.navigateEntry({ + feedId: item.feedId!, + entryId: id, + }) showEntryGaleriaAccessory({ author: item.author || "", diff --git a/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx index 6c12f6968..f645a66f5 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntrySocialItem.tsx @@ -1,4 +1,5 @@ import { FeedViewType } from "@follow/constants" +import { tracker } from "@follow/tracker" import { useCallback, useEffect, useMemo } from "react" import { Pressable, Text, View } from "react-native" import ReAnimated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated" @@ -33,12 +34,16 @@ export function EntrySocialItem({ entryId }: { entryId: string }) { const isHorizontalScrolling = getHorizontalScrolling() if (!isHorizontalScrolling) { unreadSyncService.markEntryAsRead(entryId) + tracker.navigateEntry({ + feedId: entry?.feedId!, + entryId, + }) navigation.pushControllerView(EntryDetailScreen, { entryId, view: FeedViewType.SocialMedia, }) } - }, [entryId, navigation]) + }, [entry?.feedId, entryId, navigation]) const unreadZoomSharedValue = useSharedValue(entry?.read ? 0 : 1) diff --git a/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx index 186d95f64..0eae10714 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx @@ -1,3 +1,4 @@ +import { tracker } from "@follow/tracker" import { transformVideoUrl } from "@follow/utils" import { Linking } from "react-native" @@ -25,10 +26,15 @@ export function EntryVideoItem({ id }: { id: string }) { className="m-1" onPress={() => { unreadSyncService.markEntryAsRead(id) + tracker.navigateEntry({ + feedId: item.feedId!, + entryId: id, + }) if (!item.url) { toast.error("No video URL found") return } + openVideo(item.url) }} > diff --git a/apps/mobile/src/screens/(modal)/sign-up.tsx b/apps/mobile/src/screens/(modal)/sign-up.tsx index efc8dd54b..efc9799e6 100644 --- a/apps/mobile/src/screens/(modal)/sign-up.tsx +++ b/apps/mobile/src/screens/(modal)/sign-up.tsx @@ -1,3 +1,4 @@ +import { tracker } from "@follow/tracker" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" import type { Control } from "react-hook-form" @@ -49,6 +50,7 @@ async function onSubmit(values: FormValue) { toast.error(res.error.message) } else { toast.success("Sign up successful") + tracker.register({ type: "email" }) Navigation.rootNavigation.back() } }) diff --git a/apps/mobile/src/screens/onboarding.tsx b/apps/mobile/src/screens/onboarding.tsx index d014274a4..998ab79ed 100644 --- a/apps/mobile/src/screens/onboarding.tsx +++ b/apps/mobile/src/screens/onboarding.tsx @@ -1,3 +1,4 @@ +import { tracker } from "@follow/tracker" import { useCallback, useState } from "react" import { SafeAreaView, Text, TouchableOpacity, View } from "react-native" import { SheetScreen } from "react-native-sheet-transitions" @@ -20,8 +21,10 @@ export const OnboardingScreen: NavigationControllerView = () => { const handleNext = useCallback(() => { if (currentStep < totalSteps) { setCurrentStep(currentStep + 1) + tracker.onBoarding({ step: currentStep, done: false }) } else { // Complete onboarding + tracker.onBoarding({ step: currentStep, done: true }) kv.set(isOnboardingFinishedStorageKey, "true") queryClient.invalidateQueries({ queryKey: isNewUserQueryKey }).then(() => { navigation.back() diff --git a/apps/mobile/src/store/subscription/store.ts b/apps/mobile/src/store/subscription/store.ts index fb7cc5b4d..de48089c2 100644 --- a/apps/mobile/src/store/subscription/store.ts +++ b/apps/mobile/src/store/subscription/store.ts @@ -1,4 +1,5 @@ import { FeedViewType } from "@follow/constants" +import { tracker } from "@follow/tracker" import type { SubscriptionSchema } from "@/src/database/schemas/types" import { apiClient } from "@/src/lib/api-fetch" @@ -223,6 +224,7 @@ class SubscriptionSyncService { if (data.feed) { feedActions.upsertMany([data.feed]) + tracker.subscribe({ feedId: data.feed.id, view: subscription.view }) } if (data.list) { @@ -232,8 +234,8 @@ class SubscriptionSyncService { userId: data.list.ownerUserId, }, ]) + tracker.subscribe({ listId: data.list.id, view: subscription.view }) } - // Insert to subscription subscriptionActions.upsertMany([ { diff --git a/apps/mobile/src/store/user/hooks.ts b/apps/mobile/src/store/user/hooks.ts index d8e605bf8..91abfbea3 100644 --- a/apps/mobile/src/store/user/hooks.ts +++ b/apps/mobile/src/store/user/hooks.ts @@ -1,3 +1,4 @@ +import { identifyUserOpenPanel, tracker } from "@follow/tracker" import { useQuery } from "@tanstack/react-query" import { useEffect } from "react" @@ -21,19 +22,8 @@ export const usePrefetchSessionUser = () => { useEffect(() => { if (query.data) { const user = query.data - op.identify({ - profileId: user.id, - email: user.email!, - avatar: user.image ?? undefined, - lastName: user.name!, - properties: { - handle: user.handle, - name: user.name, - }, - }) - op.track("user_login", { - userId: query.data.id, - }) + identifyUserOpenPanel(user, op.identify.bind(op)) + tracker.identify(user.id) } }, [query.data]) return query diff --git a/apps/ssr/client/initialize/analytics.ts b/apps/ssr/client/initialize/analytics.ts index 963a85570..188fb8492 100644 --- a/apps/ssr/client/initialize/analytics.ts +++ b/apps/ssr/client/initialize/analytics.ts @@ -1,32 +1,14 @@ import { env } from "@follow/shared/env" -import type { TrackProperties } from "@openpanel/web" +import { setOpenPanelTracker } from "@follow/tracker" import { op } from "./op" -declare global { - interface Window { - analytics?: { - capture: (event_name: string, properties?: TrackProperties | null) => void - reset: () => void - } - } -} export const initAnalytics = () => { if (env.VITE_OPENPANEL_CLIENT_ID === undefined) return + setOpenPanelTracker(op.track.bind(op)) op.setGlobalProperties({ build: "external-web", hash: GIT_COMMIT_SHA, }) - - window.analytics = { - reset: () => { - // op.clear() - }, - capture(event_name: string, properties?: TrackProperties | null) { - if (import.meta.env.DEV) return - - op.track(event_name, properties as TrackProperties) - }, - } } diff --git a/apps/ssr/client/initialize/helper.ts b/apps/ssr/client/initialize/helper.ts index 0b35c87ee..5d3b659c7 100644 --- a/apps/ssr/client/initialize/helper.ts +++ b/apps/ssr/client/initialize/helper.ts @@ -1,18 +1,12 @@ import type { AuthUser } from "@follow/shared/hono" +import { identifyUserOpenPanel, tracker } from "@follow/tracker" import { op } from "./op" export const setIntegrationIdentify = async (user: AuthUser) => { - op.identify({ - profileId: user.id, - email: user.email, - avatar: user.image ?? undefined, - lastName: user.name, - properties: { - handle: user.handle, - name: user.name, - }, - }) + identifyUserOpenPanel(user, op.identify.bind(op)) + tracker.identify(user.id) + await import("@sentry/react").then(({ setTag }) => { setTag("user_id", user.id) setTag("user_name", user.name) diff --git a/apps/ssr/client/pages/(login)/register.tsx b/apps/ssr/client/pages/(login)/register.tsx index d3e065de8..e90b9930f 100644 --- a/apps/ssr/client/pages/(login)/register.tsx +++ b/apps/ssr/client/pages/(login)/register.tsx @@ -11,6 +11,7 @@ import { } from "@follow/components/ui/form/index.jsx" import { Input } from "@follow/components/ui/input/index.js" import { env } from "@follow/shared/env" +import { tracker } from "@follow/tracker" import { zodResolver } from "@hookform/resolvers/zod" import { useRef } from "react" import ReCAPTCHA from "react-google-recaptcha" @@ -67,6 +68,9 @@ function RegisterForm() { callbackURL: "/", fetchOptions: { onSuccess() { + tracker.register({ + type: "email", + }) navigate("/login") }, onError(context) { diff --git a/apps/ssr/package.json b/apps/ssr/package.json index 5dacb4058..9f8d40762 100644 --- a/apps/ssr/package.json +++ b/apps/ssr/package.json @@ -11,6 +11,7 @@ "dependencies": { "@fastify/middie": "9.0.3", "@fastify/request-context": "6.1.0", + "@follow/tracker": "workspace:*", "@fontsource/sn-pro": "5.2.5", "@openpanel/web": "1.0.1", "@resvg/resvg-js": "2.6.2", diff --git a/packages/tracker/src/index.ts b/packages/tracker/src/index.ts index ebad532cf..80c8fd0bb 100644 --- a/packages/tracker/src/index.ts +++ b/packages/tracker/src/index.ts @@ -6,3 +6,29 @@ export const tracker = new TrackerPoints() export { type TrackerPoints } from "./points" export { TrackerMapper } from "./points" + +///// OpenPanel Utils + +type Nullable = T | null | undefined + +export const identifyUserOpenPanel = ( + user: { + id: string + email?: Nullable + name?: Nullable + handle?: Nullable + image?: Nullable + }, + identifyFn: (...args: any[]) => any, +) => { + identifyFn({ + profileId: user.id, + email: user.email!, + avatar: user.image ?? undefined, + lastName: user.name!, + properties: { + handle: user.handle, + name: user.name, + }, + }) +} diff --git a/packages/tracker/src/op/api.ts b/packages/tracker/src/op/api.ts index a3f687f45..054d0b775 100644 --- a/packages/tracker/src/op/api.ts +++ b/packages/tracker/src/op/api.ts @@ -3,8 +3,6 @@ interface ApiConfig { defaultHeaders?: Record> maxRetries?: number initialRetryDelay?: number - - headers?: Record> } interface FetchOptions extends RequestInit { @@ -22,7 +20,6 @@ export class Api { this.headers = { "Content-Type": "application/json", ...config.defaultHeaders, - ...config.headers, } this.maxRetries = config.maxRetries ?? 3 this.initialRetryDelay = config.initialRetryDelay ?? 500 diff --git a/packages/tracker/src/op/index.ts b/packages/tracker/src/op/index.ts index 358e29539..0af03afc3 100644 --- a/packages/tracker/src/op/index.ts +++ b/packages/tracker/src/op/index.ts @@ -68,6 +68,7 @@ export type OpenPanelOptions = { waitForProfile?: boolean filter?: (payload: TrackHandlerPayload) => boolean disabled?: boolean + headers?: Record } export class OpenPanel { @@ -79,6 +80,7 @@ export class OpenPanel { constructor(public options: OpenPanelOptions) { const defaultHeaders: Record = { "openpanel-client-id": options.clientId, + ...options.headers, } if (options.clientSecret) { diff --git a/packages/tracker/src/points.ts b/packages/tracker/src/points.ts index 0fe4e4432..6fcad319e 100644 --- a/packages/tracker/src/points.ts +++ b/packages/tracker/src/points.ts @@ -48,6 +48,14 @@ export enum TrackerMapper { FeedClaimed = 2012, DailyRewardClaimed = 2013, TipSent = 2014, + + // https://docs.google.com/spreadsheets/d/1XlUxTxiXWIQDHFYa2eoPBeuosR1t2h8VFIjXEOqmjhY/edit?gid=0#gid=0 + Register = 3000, + OnBoarding = 3001, + Subscribe = 3002, + EntryRead = 3003, + EntryAction = 3004, + ViewAction = 3005, } const CodeToTrackerName = Object.fromEntries( @@ -143,6 +151,30 @@ export class TrackerPoints { this.track(TrackerMapper.TipSent, props) } + register(props: { type: "email" | "social" }) { + this.track(TrackerMapper.Register, props) + } + + onBoarding(props: { step: number; done: boolean }) { + this.track(TrackerMapper.OnBoarding, props) + } + + subscribe(props: { feedId?: string; listId?: string; view?: number }) { + this.track(TrackerMapper.Subscribe, props) + } + + entryRead(props: { entryId: string }) { + this.track(TrackerMapper.EntryRead, props) + } + + entryAction(props: { entryId: string; action: string }) { + this.track(TrackerMapper.EntryAction, props) + } + + viewAction(props: { view: string; action: string }) { + this.track(TrackerMapper.ViewAction, props) + } + private track(code: TrackerMapper, properties?: Record) { if (code) { let name = CodeToTrackerName[code] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ead57547..7c00372c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -782,6 +782,9 @@ importers: '@follow/shared': specifier: workspace:* version: link:../../packages/shared + '@follow/tracker': + specifier: workspace:* + version: link:../../packages/tracker '@follow/utils': specifier: workspace:* version: link:../../packages/utils @@ -1107,6 +1110,9 @@ importers: '@fastify/request-context': specifier: 6.1.0 version: 6.1.0 + '@follow/tracker': + specifier: workspace:* + version: link:../../packages/tracker '@fontsource/sn-pro': specifier: 5.2.5 version: 5.2.5