feat: add some tracker

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-03-25 22:26:10 +08:00
parent 549d76285a
commit b5fc2f6a70
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
33 changed files with 189 additions and 149 deletions

View File

@ -49,7 +49,6 @@ const FeedFoundCanBeFollowErrorFallback: FC<AppErrorFallbackProps> = ({ resetErr
title: t("feed_form.add_feed"),
content: ({ dismiss }) => (
<FeedForm
asWidget
url={feed.url}
defaultValues={{
view: getRouteParams().view.toString(),

View File

@ -266,7 +266,7 @@ export const useFeedActions = ({
click: () => {
present({
title: t("sidebar.feed_actions.edit_feed"),
content: ({ dismiss }) => <FeedForm asWidget id={feedId} onSuccess={dismiss} />,
content: ({ dismiss }) => <FeedForm id={feedId} onSuccess={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 }) => <ListForm asWidget id={listId} onSuccess={dismiss} />,
content: ({ dismiss }) => <ListForm id={listId} onSuccess={dismiss} />,
})
},
},

View File

@ -28,9 +28,9 @@ export const usePresentFeedFormModal = () => {
title: isList ? t("sidebar.feed_actions.edit_list") : t("sidebar.feed_actions.edit_feed"),
content: ({ dismiss }) =>
isList ? (
<ListForm asWidget id={params.listId} onSuccess={dismiss} />
<ListForm id={params.listId} onSuccess={dismiss} />
) : (
<FeedForm asWidget id={params.feedId} onSuccess={dismiss} />
<FeedForm id={params.feedId} onSuccess={dismiss} />
),
})
} else {

View File

@ -87,14 +87,12 @@ export const useFollow = () => {
}
return options?.isList ? (
<ListForm
asWidget
id={options?.id}
defaultValues={options?.defaultValues as ListFormDataValuesType}
onSuccess={onSuccess}
/>
) : (
<FeedForm
asWidget
id={options?.id}
url={options?.url}
defaultValues={options?.defaultValues as FeedFormDataValuesType}

View File

@ -1,18 +1,10 @@
import type { UserModel } from "@follow/models"
import { identifyUserOpenPanel } from "@follow/tracker"
import { op } from "./op"
export const setIntegrationIdentify = async (user: UserModel) => {
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,
})

View File

@ -28,7 +28,7 @@ export const useRegisterListCommands = () => {
if (!listId) return
present({
title: t("sidebar.feed_actions.edit_list"),
content: ({ dismiss }) => <ListForm asWidget id={listId} onSuccess={dismiss} />,
content: ({ dismiss }) => <ListForm id={listId} onSuccess={dismiss} />,
})
},
},

View File

@ -203,7 +203,6 @@ export const DiscoverFeedForm = ({
title: t("feed_form.add_feed"),
content: () => (
<FeedForm
asWidget
url={finalUrl}
defaultValues={{
view: defaultView.toString(),

View File

@ -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 { EntryModelSimple, FeedModel } 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"
@ -54,9 +54,9 @@ export const FeedForm: Component<{
url?: string
id?: string
defaultValues?: FeedFormDataValuesType
asWidget?: boolean
onSuccess?: () => 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 (
<div
className={cn(
"flex h-full flex-col",
asWidget
? "mx-auto min-h-[420px] w-full max-w-[550px] lg:min-w-[550px]"
: "px-[18px] pb-[18px] pt-12",
"mx-auto min-h-[420px] w-full max-w-[550px] lg:min-w-[550px]",
)}
>
{!asWidget && (
<div className="mb-4 mt-2 flex items-center gap-2 text-[22px] font-bold">
<Logo className="size-8" />
{isSubscribed ? t("feed_form.update_follow") : t("feed_form.add_follow")}
</div>
)}
{feed ? (
<FeedInnerForm
{...{
defaultValues,
id,
url,
asWidget,
onSuccess,
subscriptionData: feedQuery.data?.subscription,
entries: feedQuery.data?.entries,
@ -167,7 +155,7 @@ export const FeedForm: Component<{
const FeedInnerForm = ({
defaultValues,
id,
asWidget,
onSuccess,
subscriptionData,
feed,
@ -175,7 +163,7 @@ const FeedInnerForm = ({
}: {
defaultValues?: z.infer<typeof formSchema>
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?.()

View File

@ -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 (
<div
className={cn(
"flex h-full flex-col",
asWidget
? "mx-auto min-h-[420px] w-full max-w-[550px] lg:min-w-[550px]"
: "px-[18px] pb-[18px] pt-12",
"mx-auto min-h-[420px] w-full max-w-[550px] lg:min-w-[550px]",
)}
>
{!asWidget && (
<div className="mb-4 mt-2 flex items-center gap-2 text-[22px] font-bold">
<Logo className="size-8" />
{isSubscribed ? t("feed_form.update_follow") : t("feed_form.add_follow")}
</div>
)}
{list ? (
<ListInnerForm
{...{
defaultValues,
id,
asWidget,
onSuccess,
subscriptionData: feedQuery.data?.subscription,
list,
@ -164,14 +150,14 @@ export const ListForm: Component<{
const ListInnerForm = ({
defaultValues,
id,
asWidget,
onSuccess,
subscriptionData,
list,
}: {
defaultValues?: z.infer<typeof formSchema>
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?.()

View File

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

View File

@ -55,7 +55,6 @@ const CmdNPanel = () => {
title: t("feed_form.add_feed"),
content: () => (
<FeedForm
asWidget
url={url}
defaultValues={{
view: defaultView.toString(),

View File

@ -1,32 +0,0 @@
import { FeedViewType } from "@follow/constants"
import { useLayoutEffect } from "react"
import { useSearchParams } from "react-router"
import { FeedForm } from "~/modules/discover/feed-form"
export function Component() {
const [urlSearchParams] = useSearchParams()
const paramUrl = urlSearchParams.get("url")
const url = paramUrl ? decodeURIComponent(paramUrl) : undefined
const id = urlSearchParams.get("id") || undefined
const defaultView = urlSearchParams.get("view") || FeedViewType.Articles
useLayoutEffect(() => {
document.body.style.overflow = "hidden"
return () => {
document.body.style.overflow = ""
}
}, [])
return (
<div className="bg-theme-background h-full overflow-hidden">
<FeedForm
url={url}
id={id}
defaultValues={{
view: defaultView.toString(),
}}
/>
</div>
)
}

View File

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

View File

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

View File

@ -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> | any) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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([
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,3 +6,29 @@ export const tracker = new TrackerPoints()
export { type TrackerPoints } from "./points"
export { TrackerMapper } from "./points"
///// OpenPanel Utils
type Nullable<T> = T | null | undefined
export const identifyUserOpenPanel = (
user: {
id: string
email?: Nullable<string>
name?: Nullable<string>
handle?: Nullable<string>
image?: Nullable<string>
},
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,
},
})
}

View File

@ -3,8 +3,6 @@ interface ApiConfig {
defaultHeaders?: Record<string, string | Promise<string | null>>
maxRetries?: number
initialRetryDelay?: number
headers?: Record<string, string | Promise<string | null>>
}
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

View File

@ -68,6 +68,7 @@ export type OpenPanelOptions = {
waitForProfile?: boolean
filter?: (payload: TrackHandlerPayload) => boolean
disabled?: boolean
headers?: Record<string, string>
}
export class OpenPanel {
@ -79,6 +80,7 @@ export class OpenPanel {
constructor(public options: OpenPanelOptions) {
const defaultHeaders: Record<string, string> = {
"openpanel-client-id": options.clientId,
...options.headers,
}
if (options.clientSecret) {

View File

@ -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<string, unknown>) {
if (code) {
let name = CodeToTrackerName[code]

View File

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