refactor(tracker): add tracker package
Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
359093dcf1
commit
549d76285a
|
|
@ -1,16 +0,0 @@
|
|||
import { op } from "~/lib/op"
|
||||
|
||||
const PREFIX = "app:"
|
||||
export const hotUpdateDownloadTrack = (version: string) => {
|
||||
op.track(`${PREFIX}hot-update-download`, { version })
|
||||
}
|
||||
export const hotUpdateAppNotSupportTriggerTrack = (data: {
|
||||
appVersion: string
|
||||
manifestVersion: string
|
||||
}) => {
|
||||
op.track(`${PREFIX}hot-update-app-not-support-trigger`, data)
|
||||
}
|
||||
|
||||
export const hotUpdateRenderSuccessTrack = (version: string) => {
|
||||
op.track(`${PREFIX}hot-update-render-success`, { version })
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ import { load } from "js-yaml"
|
|||
import { x } from "tar"
|
||||
|
||||
import { GITHUB_OWNER, GITHUB_REPO, HOTUPDATE_RENDER_ENTRY_DIR } from "~/constants/app"
|
||||
import { hotUpdateDownloadTrack, hotUpdateRenderSuccessTrack } from "~/tracker"
|
||||
import { getMainWindow } from "~/window"
|
||||
|
||||
import { appUpdaterConfig } from "./configs"
|
||||
|
|
@ -134,7 +133,6 @@ export const canUpdateRender = async (): Promise<[CanUpdateRenderState, Manifest
|
|||
return [CanUpdateRenderState.NEEDED, manifest]
|
||||
}
|
||||
const downloadRenderAsset = async (manifest: Manifest) => {
|
||||
hotUpdateDownloadTrack(manifest.version)
|
||||
const { filename } = manifest
|
||||
const url = await getFileDownloadUrl(filename)
|
||||
|
||||
|
|
@ -181,7 +179,7 @@ export const hotUpdateRender = async (manifest: Manifest) => {
|
|||
JSON.stringify(manifest),
|
||||
)
|
||||
logger.info(`Hot update render success, update to ${manifest.version}`)
|
||||
hotUpdateRenderSuccessTrack(manifest.version)
|
||||
|
||||
const mainWindow = getMainWindow()
|
||||
if (!mainWindow) return false
|
||||
const caller = callWindowExpose(mainWindow)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@electron-toolkit/preload": "3.0.1",
|
||||
"@follow/electron-main": "workspace:*",
|
||||
"@follow/shared": "workspace:*",
|
||||
"@follow/tracker": "workspace:*",
|
||||
"@fontsource/sn-pro": "5.2.5",
|
||||
"@headlessui/react": "2.2.0",
|
||||
"@hookform/resolvers": "4.1.3",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { isMobile } from "@follow/components/hooks/useMobile.js"
|
||||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { cn, getOS } from "@follow/utils/utils"
|
||||
import { useEffect } from "react"
|
||||
import { Outlet } from "react-router"
|
||||
|
|
@ -58,9 +59,7 @@ const AppLayer = () => {
|
|||
removeAppSkeleton()
|
||||
|
||||
const doneTime = Math.trunc(performance.now())
|
||||
window.analytics?.capture("ui_render_init", {
|
||||
time: doneTime,
|
||||
})
|
||||
tracker.uiRenderInit(doneTime)
|
||||
appLog("App is ready", `${doneTime}ms`)
|
||||
|
||||
applyAfterReadyCallbacks()
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
import type { TrackerPoints } from "@follow/tracker"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import type { AllTrackers } from "@follow/tracker/src/points"
|
||||
import { memo, useState } from "react"
|
||||
import { useInView } from "react-intersection-observer"
|
||||
|
||||
type ImpressionProps = {
|
||||
event: string
|
||||
type ImpressionProps<T extends AllTrackers> = {
|
||||
event: T
|
||||
onTrack?: () => any
|
||||
properties?: Record<string, any>
|
||||
properties?: Parameters<TrackerPoints[T]>
|
||||
children: React.ReactNode
|
||||
}
|
||||
export const ImpressionView: Component<{ shouldTrack?: boolean } & ImpressionProps> = (props) => {
|
||||
|
||||
export function ImpressionView<T extends keyof typeof tracker>(
|
||||
props: ImpressionProps<T> & { shouldTrack?: boolean },
|
||||
) {
|
||||
const { shouldTrack = true, ...rest } = props
|
||||
if (!shouldTrack) {
|
||||
return <>{props.children}</>
|
||||
}
|
||||
return <ImpressionViewImpl {...rest} />
|
||||
return <MemoImpressionViewImpl {...rest} />
|
||||
}
|
||||
|
||||
const ImpressionViewImpl: Component<ImpressionProps> = memo((props) => {
|
||||
function ImpressionViewImpl<T extends keyof typeof tracker>(props: ImpressionProps<T>) {
|
||||
const [impression, setImpression] = useState(false)
|
||||
|
||||
const { ref } = useInView({
|
||||
|
|
@ -26,10 +33,8 @@ const ImpressionViewImpl: Component<ImpressionProps> = memo((props) => {
|
|||
}
|
||||
setImpression(true)
|
||||
|
||||
window.analytics?.capture(props.event, {
|
||||
impression: 1,
|
||||
...props.properties,
|
||||
})
|
||||
// @ts-expect-error
|
||||
tracker[props.event]?.apply(null, props.properties)
|
||||
props.onTrack?.()
|
||||
},
|
||||
})
|
||||
|
|
@ -40,6 +45,6 @@ const ImpressionViewImpl: Component<ImpressionProps> = memo((props) => {
|
|||
{!impression && <span ref={ref} />}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
ImpressionViewImpl.displayName = "ImpressionView"
|
||||
}
|
||||
const MemoImpressionViewImpl = memo(ImpressionViewImpl)
|
||||
MemoImpressionViewImpl.displayName = "ImpressionView"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { getReadonlyRoute, getStableRouterNavigate } from "@follow/components/at
|
|||
import { useMobile } from "@follow/components/hooks/useMobile.js"
|
||||
import { useSheetContext } from "@follow/components/ui/sheet/context.js"
|
||||
import type { FeedViewType } from "@follow/constants"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import { disableShowAISummary } from "~/atoms/ai-summary"
|
||||
|
|
@ -81,13 +82,11 @@ export const navigateEntry = (options: NavigateEntryOptions) => {
|
|||
disableShowAISummary()
|
||||
disableShowAITranslation()
|
||||
|
||||
if (window.analytics) {
|
||||
window.analytics.capture("Navigate Entry", {
|
||||
feedId: finalFeedId,
|
||||
entryId,
|
||||
timelineId: finalTimelineId,
|
||||
})
|
||||
}
|
||||
tracker.navigateEntry({
|
||||
feedId: finalFeedId,
|
||||
entryId: finalEntryId,
|
||||
timelineId: finalTimelineId,
|
||||
})
|
||||
|
||||
const path = `/timeline/${finalTimelineId}/${finalFeedId}/${finalEntryId}`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { env } from "@follow/shared/env.desktop"
|
||||
import { setOpenPanelTracker } from "@follow/tracker"
|
||||
import type { TrackProperties } from "@openpanel/web"
|
||||
|
||||
import { getGeneralSettings } from "~/atoms/settings/general"
|
||||
|
|
@ -6,14 +7,6 @@ import { whoami } from "~/atoms/user"
|
|||
|
||||
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
|
||||
|
||||
|
|
@ -23,21 +16,16 @@ export const initAnalytics = () => {
|
|||
hash: GIT_COMMIT_SHA,
|
||||
})
|
||||
|
||||
window.analytics = {
|
||||
reset: () => {
|
||||
// op.clear()
|
||||
},
|
||||
capture(event_name: string, properties?: TrackProperties | null) {
|
||||
if (import.meta.env.DEV) return
|
||||
if (!getGeneralSettings().sendAnonymousData) {
|
||||
return
|
||||
}
|
||||
const me = whoami()
|
||||
setOpenPanelTracker(async (name, properties) => {
|
||||
if (import.meta.env.DEV) return
|
||||
if (!getGeneralSettings().sendAnonymousData) {
|
||||
return
|
||||
}
|
||||
const me = whoami()
|
||||
|
||||
op.track(event_name, {
|
||||
...properties,
|
||||
user_id: me?.id,
|
||||
} as TrackProperties)
|
||||
},
|
||||
}
|
||||
return op.track(name, {
|
||||
...properties,
|
||||
user_id: me?.id,
|
||||
} as TrackProperties)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { initializeDayjs } from "@follow/components/dayjs"
|
||||
import { registerGlobalContext } from "@follow/shared/bridge"
|
||||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { repository } from "@pkg"
|
||||
import { enableMapSet } from "immer"
|
||||
|
||||
|
|
@ -115,14 +116,14 @@ export const initializeApp = async () => {
|
|||
const loadingTime = Date.now() - now
|
||||
appLog(`Initialize ${APP_NAME} done,`, `${loadingTime}ms`)
|
||||
|
||||
window.analytics?.capture("app_init", {
|
||||
tracker.appInit({
|
||||
electron: IN_ELECTRON,
|
||||
loading_time: loadingTime,
|
||||
using_indexed_db: enabledDataPersist,
|
||||
data_hydrated_time: dataHydratedTime,
|
||||
version: APP_VERSION,
|
||||
rn: false,
|
||||
})
|
||||
|
||||
// Options for react-google-recaptcha
|
||||
window.recaptchaOptions = {
|
||||
useRecaptchaNet: true,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { tracker } from "@follow/tracker"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
|
||||
|
|
@ -48,7 +49,8 @@ export const useBoostFeedMutation = () => {
|
|||
query.getStatus({ feedId: variables.feedId }).invalidate()
|
||||
query.getBoosters({ feedId: variables.feedId }).invalidate()
|
||||
updateFeedBoostStatus(variables.feedId, true)
|
||||
window.analytics?.capture("boost_sent", {
|
||||
|
||||
tracker.boostSent({
|
||||
amount: variables.amount,
|
||||
feedId: variables.feedId,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
SimpleIconsReadwise,
|
||||
} from "@follow/components/ui/platform-icon/icons.js"
|
||||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import type { FetchError } from "ofetch"
|
||||
import { ofetch } from "ofetch"
|
||||
|
|
@ -123,7 +124,7 @@ const useRegisterReadwiseCommands = () => {
|
|||
return
|
||||
}
|
||||
try {
|
||||
window.analytics?.capture("integration", {
|
||||
tracker.integration({
|
||||
type: "readwise",
|
||||
event: "save",
|
||||
})
|
||||
|
|
@ -194,7 +195,7 @@ const useRegisterInstapaperCommands = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
window.analytics?.capture("integration", {
|
||||
tracker.integration({
|
||||
type: "instapaper",
|
||||
event: "save",
|
||||
})
|
||||
|
|
@ -300,7 +301,7 @@ const useRegisterObsidianCommands = () => {
|
|||
return
|
||||
}
|
||||
const markdownContent = await getEntryContentAsMarkdown(entry)
|
||||
window.analytics?.capture("integration", {
|
||||
tracker.integration({
|
||||
type: "obsidian",
|
||||
event: "save",
|
||||
})
|
||||
|
|
@ -407,7 +408,7 @@ const useRegisterReadeckCommands = () => {
|
|||
return
|
||||
}
|
||||
try {
|
||||
window.analytics?.capture("integration", {
|
||||
tracker.integration({
|
||||
type: "readeck",
|
||||
event: "save",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ActionButton } from "@follow/components/ui/button/index.js"
|
|||
import { DividerVertical } from "@follow/components/ui/divider/Divider.js"
|
||||
import { SegmentGroup, SegmentItem } from "@follow/components/ui/segment/index.js"
|
||||
import { Slider } from "@follow/components/ui/slider/index.js"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { clsx, cn } from "@follow/utils/utils"
|
||||
import {
|
||||
HoverCard,
|
||||
|
|
@ -23,7 +24,6 @@ import {
|
|||
useSetZenMode,
|
||||
useUISettingKey,
|
||||
} from "~/atoms/settings/ui"
|
||||
import { ImpressionView } from "~/components/common/ImpressionTracker"
|
||||
import { shortcuts } from "~/constants/shortcuts"
|
||||
import { useAIDailyReportModal } from "~/modules/ai/ai-daily/useAIDailyReportModal"
|
||||
|
||||
|
|
@ -34,19 +34,15 @@ export const DailyReportButton: FC = () => {
|
|||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<ImpressionView event="Daily Report Modal">
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
present()
|
||||
window.analytics?.capture("Daily Report Modal", {
|
||||
click: 1,
|
||||
})
|
||||
}}
|
||||
tooltip={t("entry_list_header.daily_report")}
|
||||
>
|
||||
<i className="i-mgc-magic-2-cute-re" />
|
||||
</ActionButton>
|
||||
</ImpressionView>
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
present()
|
||||
tracker.dailyReportModal()
|
||||
}}
|
||||
tooltip={t("entry_list_header.daily_report")}
|
||||
>
|
||||
<i className="i-mgc-magic-2-cute-re" />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -150,44 +146,34 @@ export const WideModeButton = () => {
|
|||
|
||||
const setIsZenMode = useSetZenMode()
|
||||
return (
|
||||
<ImpressionView
|
||||
event="Switch to Wide Mode"
|
||||
properties={{
|
||||
wideMode: isWideMode ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<ActionButton
|
||||
shortcut={shortcuts.layout.toggleWideMode.key}
|
||||
onClick={() => {
|
||||
if (isZenMode) {
|
||||
setIsZenMode(false)
|
||||
} else {
|
||||
setUISetting("wideMode", !isWideMode)
|
||||
// TODO: Remove this after useMeasure can get bounds in time
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}
|
||||
window.analytics?.capture("Switch to Wide Mode", {
|
||||
wideMode: !isWideMode ? 1 : 0,
|
||||
click: 1,
|
||||
})
|
||||
}}
|
||||
tooltip={
|
||||
isZenMode
|
||||
? t("zen.exit")
|
||||
: !isWideMode
|
||||
? t("entry_list_header.switch_to_widemode")
|
||||
: t("entry_list_header.switch_to_normalmode")
|
||||
<ActionButton
|
||||
shortcut={shortcuts.layout.toggleWideMode.key}
|
||||
onClick={() => {
|
||||
if (isZenMode) {
|
||||
setIsZenMode(false)
|
||||
} else {
|
||||
setUISetting("wideMode", !isWideMode)
|
||||
// TODO: Remove this after useMeasure can get bounds in time
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
}
|
||||
>
|
||||
{isZenMode ? (
|
||||
<MdiMeditation />
|
||||
) : (
|
||||
<i
|
||||
className={cn(isWideMode ? "i-mgc-align-justify-cute-re" : "i-mgc-align-left-cute-re")}
|
||||
/>
|
||||
)}
|
||||
</ActionButton>
|
||||
</ImpressionView>
|
||||
tracker.wideMode({ mode: isWideMode ? "wide" : "normal" })
|
||||
}}
|
||||
tooltip={
|
||||
isZenMode
|
||||
? t("zen.exit")
|
||||
: !isWideMode
|
||||
? t("entry_list_header.switch_to_widemode")
|
||||
: t("entry_list_header.switch_to_normalmode")
|
||||
}
|
||||
>
|
||||
{isZenMode ? (
|
||||
<MdiMeditation />
|
||||
) : (
|
||||
<i
|
||||
className={cn(isWideMode ? "i-mgc-align-justify-cute-re" : "i-mgc-align-left-cute-re")}
|
||||
/>
|
||||
)}
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ActionButton } from "@follow/components/ui/button/index.js"
|
||||
import type { MediaModel } from "@follow/models"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
|
|
@ -15,7 +16,9 @@ export const ImageGalleryAction = ({ id }: { id: string }) => {
|
|||
return (
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
window.analytics?.capture("entry_content_header_image_gallery_click")
|
||||
tracker.entryContentHeaderImageGalleryClick({
|
||||
feedId: id,
|
||||
})
|
||||
present({
|
||||
title: t("entry_actions.image_gallery"),
|
||||
content: () => <ImageGallery images={images as any as MediaModel[]} />,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.jsx"
|
||||
import type { FeedViewType } from "@follow/constants"
|
||||
import { useInputComposition } from "@follow/hooks"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { clsx, cn } from "@follow/utils/utils"
|
||||
import { Command } from "cmdk"
|
||||
import type { FC } from "react"
|
||||
|
|
@ -46,7 +47,8 @@ export const SearchCmdK: React.FC = () => {
|
|||
React.useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
window.analytics?.capture("search_open")
|
||||
tracker.searchOpen()
|
||||
|
||||
// Refresh data
|
||||
setPage(0)
|
||||
setSearchInstance(() => searchActions.createLocalDbSearch())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Form, FormControl, FormField, FormItem } from "@follow/components/ui/form/index.jsx"
|
||||
import { useRegisterGlobalContext } from "@follow/shared/bridge"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useLayoutEffect } from "react"
|
||||
|
|
@ -45,7 +46,10 @@ const CmdNPanel = () => {
|
|||
|
||||
const defaultView = getRouteParams().view
|
||||
|
||||
window.analytics?.capture("quick_add_feed", { url, defaultView })
|
||||
tracker.quickAddFeed({
|
||||
type: "url",
|
||||
defaultView: Number(defaultView),
|
||||
})
|
||||
|
||||
present({
|
||||
title: t("feed_form.add_feed"),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useFocusable } from "@follow/components/common/Focusable.js"
|
|||
import { useMobile } from "@follow/components/hooks/useMobile.js"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.jsx"
|
||||
import { FeedViewType } from "@follow/constants"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import * as Slider from "@radix-ui/react-slider"
|
||||
import dayjs from "dayjs"
|
||||
|
|
@ -76,7 +77,8 @@ const usePlayerTracker = () => {
|
|||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const playerState = getAudioPlayerAtomValue()
|
||||
window.analytics?.capture("player_open_duration", {
|
||||
|
||||
tracker.playerOpenDuration({
|
||||
duration: Date.now() - playerOpenAt,
|
||||
status: playerState.status,
|
||||
trigger: "beforeunload",
|
||||
|
|
@ -90,7 +92,7 @@ const usePlayerTracker = () => {
|
|||
useEffect(() => {
|
||||
if (!show) {
|
||||
const playerState = getAudioPlayerAtomValue()
|
||||
window.analytics?.capture("player_open_duration", {
|
||||
tracker.playerOpenDuration({
|
||||
duration: Date.now() - playerOpenAt,
|
||||
status: playerState.status,
|
||||
trigger: "manual",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { tracker } from "@follow/tracker"
|
||||
import { cn } from "@follow/utils/utils"
|
||||
import { m, useMotionTemplate, useMotionValue } from "framer-motion"
|
||||
import { useCallback, useEffect } from "react"
|
||||
|
|
@ -26,7 +27,7 @@ export const UpdateNotice = () => {
|
|||
const handleClick = useCallback(() => {
|
||||
const status = getUpdaterStatus()
|
||||
if (!status) return
|
||||
window.analytics?.capture("update_restart", {
|
||||
tracker.updateRestart({
|
||||
type: status.type,
|
||||
})
|
||||
switch (status.type) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { tracker } from "@follow/tracker"
|
||||
import { createElement, useCallback } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
|
@ -24,7 +25,7 @@ export const useTipModal = () => {
|
|||
toast.error("Invalid feed id or entry id")
|
||||
return
|
||||
}
|
||||
window.analytics?.capture("tip_modal_opened", { entryId })
|
||||
tracker.tipModalOpened({ entryId })
|
||||
present({
|
||||
title: t("tip_modal.tip_title"),
|
||||
content: () => createElement(TipModalContent, { userId, feedId, entryId }),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { tracker } from "@follow/tracker"
|
||||
import { formatXml } from "@follow/utils/utils"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useRef } from "react"
|
||||
|
|
@ -64,7 +65,7 @@ export const useClaimFeedMutation = (feedId: string) =>
|
|||
toastFetchError(err)
|
||||
},
|
||||
onSuccess() {
|
||||
window.analytics?.capture("feed_claimed", {
|
||||
tracker.feedClaimed({
|
||||
feedId,
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { tracker } from "@follow/tracker"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { useNavigate } from "react-router"
|
||||
import { toast } from "sonner"
|
||||
|
|
@ -109,7 +110,7 @@ export const useClaimWalletDailyRewardMutation = () => {
|
|||
onSuccess() {
|
||||
wallet.get().invalidate()
|
||||
wallet.claimCheck().invalidate()
|
||||
window.analytics?.capture("daily_reward_claimed")
|
||||
tracker.dailyRewardClaimed()
|
||||
|
||||
toast(
|
||||
<div className="flex items-center gap-1 text-lg" onClick={() => navigate("/power")}>
|
||||
|
|
@ -138,7 +139,7 @@ export const useWalletTipMutation = () =>
|
|||
onSuccess(_, variables) {
|
||||
wallet.get().invalidate()
|
||||
wallet.transactions.get().invalidate()
|
||||
window.analytics?.capture("tip_sent", {
|
||||
tracker.tipSent({
|
||||
amount: variables.amount,
|
||||
entryId: variables.entryId,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"@follow/utils": "workspace:*",
|
||||
"@gorhom/portal": "1.0.14",
|
||||
"@hookform/resolvers": "4.1.3",
|
||||
"@openpanel/sdk": "1.0.0",
|
||||
"@react-native-community/image-editor": "4.3.0",
|
||||
"@react-native-firebase/analytics": "21.12.0",
|
||||
"@react-native-firebase/app": "21.12.0",
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
"es-toolkit": "1.33.0",
|
||||
"expo": "52.0.39",
|
||||
"expo-apple-authentication": "7.1.3",
|
||||
"expo-application": "6.0.2",
|
||||
"expo-av": "15.0.2",
|
||||
"expo-blur": "14.0.3",
|
||||
"expo-build-properties": "0.13.2",
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import { forwardRef, useCallback, useMemo, useState } from "react"
|
|||
|
||||
import { proxyEnv } from "@/src/lib/proxy-env"
|
||||
|
||||
const buildSafeHeaders = createBuildSafeHeaders(proxyEnv.VITE_WEB_URL, [
|
||||
const buildSafeHeaders = createBuildSafeHeaders(proxyEnv.WEB_URL, [
|
||||
IMAGE_PROXY_URL,
|
||||
proxyEnv.VITE_API_URL,
|
||||
proxyEnv.API_URL,
|
||||
])
|
||||
|
||||
export type ImageProps = Omit<ExpoImageProps, "source"> & {
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import { OpenPanel } from "@openpanel/web"
|
||||
|
||||
export const op = new OpenPanel({
|
||||
clientId: process.env.EXPO_PUBLIC_OPENPANEL_CLIENT_ID ?? "",
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
apiUrl: process.env.EXPO_PUBLIC_OPENPANEL_API_URL,
|
||||
})
|
||||
|
|
@ -11,7 +11,7 @@ const { hc } = require("hono/dist/cjs/client/client") as typeof import("hono/cli
|
|||
export const apiFetch = ofetch.create({
|
||||
retry: false,
|
||||
|
||||
baseURL: proxyEnv.VITE_API_URL,
|
||||
baseURL: proxyEnv.API_URL,
|
||||
onRequest: async (ctx) => {
|
||||
const { options, request } = ctx
|
||||
if (__DEV__) {
|
||||
|
|
@ -47,7 +47,7 @@ export const apiFetch = ofetch.create({
|
|||
},
|
||||
})
|
||||
|
||||
export const apiClient = hc<AppType>(proxyEnv.VITE_API_URL, {
|
||||
export const apiClient = hc<AppType>(proxyEnv.API_URL, {
|
||||
fetch: async (input: any, options = {}) =>
|
||||
apiFetch(input.toString(), options).catch((err) => {
|
||||
throw err
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const serverPlugins = [
|
|||
] satisfies BetterAuthClientPlugin[]
|
||||
|
||||
const authClient = createAuthClient({
|
||||
baseURL: `${proxyEnv.VITE_API_URL}/better-auth`,
|
||||
baseURL: `${proxyEnv.API_URL}/better-auth`,
|
||||
plugins: [
|
||||
twoFactorClient(),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { OpenPanel } from "@openpanel/sdk"
|
||||
|
||||
import { proxyEnv } from "./proxy-env"
|
||||
|
||||
export const op = new OpenPanel({
|
||||
clientId: proxyEnv.OPENPANEL_CLIENT_ID ?? "",
|
||||
|
||||
apiUrl: proxyEnv.OPENPANEL_API_URL,
|
||||
// waitForProfile: true,
|
||||
})
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { env } from "@follow/shared/src/env.rn"
|
||||
import { envProfileMap } from "@follow/shared/src/env.rn"
|
||||
import type { env, envProfileMap } from "@follow/shared/src/env.rn"
|
||||
import { getEnvProfiles__dangerously } from "@follow/shared/src/env.rn"
|
||||
import { createAtomHooks } from "@follow/utils"
|
||||
import { reloadAppAsync } from "expo"
|
||||
import { atomWithStorage } from "jotai/utils"
|
||||
|
|
@ -21,7 +21,10 @@ export const proxyEnv = new Proxy(
|
|||
{
|
||||
get(target, prop) {
|
||||
const profile = getEnvProfile() as keyof typeof envProfileMap
|
||||
return envProfileMap[profile][prop as keyof (typeof envProfileMap)[typeof profile]]
|
||||
const envProfiles = getEnvProfiles__dangerously()
|
||||
const envMap = envProfiles[profile]
|
||||
|
||||
return envMap[prop as keyof typeof envMap]
|
||||
},
|
||||
},
|
||||
) as any as typeof env
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Constants from "expo-constants"
|
||||
import { nativeApplicationVersion, nativeBuildVersion } from "expo-application"
|
||||
import { Linking, Text, View } from "react-native"
|
||||
|
||||
import { Link } from "@/src/components/common/Link"
|
||||
|
|
@ -43,8 +43,8 @@ const links = [
|
|||
]
|
||||
|
||||
export const AboutScreen = () => {
|
||||
const buildId = Constants.expoConfig?.extra?.eas?.buildId || "Development"
|
||||
const appVersion = Constants.expoConfig?.version || "0.0.0"
|
||||
const buildId = nativeBuildVersion
|
||||
const appVersion = nativeApplicationVersion
|
||||
|
||||
return (
|
||||
<SafeNavigationScrollView className="bg-system-grouped-background" contentViewClassName="pt-6">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useEffect } from "react"
|
|||
import { apiClient } from "@/src/lib/api-fetch"
|
||||
import { kv } from "@/src/lib/kv"
|
||||
import { useNavigation } from "@/src/lib/navigation/hooks"
|
||||
import { op } from "@/src/lib/op"
|
||||
import { OnboardingScreen } from "@/src/screens/onboarding"
|
||||
|
||||
import { isNewUserQueryKey, isOnboardingFinishedStorageKey } from "./constants"
|
||||
|
|
@ -12,10 +13,30 @@ import { userSyncService, useUserStore } from "./store"
|
|||
export const whoamiQueryKey = ["user", "whoami"]
|
||||
|
||||
export const usePrefetchSessionUser = () => {
|
||||
useQuery({
|
||||
const query = useQuery({
|
||||
queryKey: whoamiQueryKey,
|
||||
queryFn: () => userSyncService.whoami(),
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
}, [query.data])
|
||||
return query
|
||||
}
|
||||
|
||||
export const useWhoami = () => {
|
||||
|
|
|
|||
|
|
@ -3,26 +3,36 @@
|
|||
*/
|
||||
const profile = "prod"
|
||||
|
||||
export const envProfileMap = {
|
||||
const envProfileMap = {
|
||||
prod: {
|
||||
VITE_API_URL: "https://api.follow.is",
|
||||
VITE_WEB_URL: "https://app.follow.is",
|
||||
VITE_INBOXES_EMAIL: "@follow.re",
|
||||
API_URL: "https://api.follow.is",
|
||||
WEB_URL: "https://app.follow.is",
|
||||
INBOXES_EMAIL: "@follow.re",
|
||||
OPENPANEL_CLIENT_ID: "0e477ab4-d92d-4d6e-b889-b09d86ab908e",
|
||||
OPENPANEL_API_URL: "https://openpanel.follow.is/api",
|
||||
},
|
||||
dev: {
|
||||
VITE_API_URL: "https://api.dev.follow.is",
|
||||
VITE_WEB_URL: "https://dev.follow.is",
|
||||
VITE_INBOXES_EMAIL: "__devdev@follow.re",
|
||||
API_URL: "https://api.dev.follow.is",
|
||||
WEB_URL: "https://dev.follow.is",
|
||||
INBOXES_EMAIL: "__dev@follow.re",
|
||||
},
|
||||
staging: {
|
||||
VITE_API_URL: "https://api.follow.is",
|
||||
VITE_WEB_URL: "https://staging.follow.is",
|
||||
VITE_INBOXES_EMAIL: "@follow.re",
|
||||
API_URL: "https://api.follow.is",
|
||||
WEB_URL: "https://staging.follow.is",
|
||||
INBOXES_EMAIL: "@follow.re",
|
||||
OPENPANEL_CLIENT_ID: "0e477ab4-d92d-4d6e-b889-b09d86ab908e",
|
||||
OPENPANEL_API_URL: "https://openpanel.follow.is/api",
|
||||
},
|
||||
}
|
||||
|
||||
export const getEnvProfiles__dangerously = () => envProfileMap
|
||||
export type { envProfileMap }
|
||||
/**
|
||||
* @description this env always use prod env, please use `proxyEnv` to access dynamic env
|
||||
*/
|
||||
export const env = {
|
||||
VITE_WEB_URL: envProfileMap[profile].VITE_WEB_URL,
|
||||
VITE_API_URL: envProfileMap[profile].VITE_API_URL,
|
||||
WEB_URL: envProfileMap[profile].WEB_URL,
|
||||
API_URL: envProfileMap[profile].API_URL,
|
||||
APP_CHECK_DEBUG_TOKEN: process.env.EXPO_PUBLIC_APP_CHECK_DEBUG_TOKEN,
|
||||
OPENPANEL_CLIENT_ID: envProfileMap[profile].OPENPANEL_CLIENT_ID,
|
||||
OPENPANEL_API_URL: envProfileMap[profile].OPENPANEL_API_URL,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@follow/tracker",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { TrackerPoints, trackManager } from "./points"
|
||||
|
||||
export const setOpenPanelTracker = trackManager.setTrackFn.bind(trackManager)
|
||||
|
||||
export const tracker = new TrackerPoints()
|
||||
|
||||
export { type TrackerPoints } from "./points"
|
||||
export { TrackerMapper } from "./points"
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
interface ApiConfig {
|
||||
baseUrl: string
|
||||
defaultHeaders?: Record<string, string | Promise<string | null>>
|
||||
maxRetries?: number
|
||||
initialRetryDelay?: number
|
||||
|
||||
headers?: Record<string, string | Promise<string | null>>
|
||||
}
|
||||
|
||||
interface FetchOptions extends RequestInit {
|
||||
retries?: number
|
||||
}
|
||||
|
||||
export class Api {
|
||||
private baseUrl: string
|
||||
private headers: Record<string, string | Promise<string | null>>
|
||||
private maxRetries: number
|
||||
private initialRetryDelay: number
|
||||
|
||||
constructor(config: ApiConfig) {
|
||||
this.baseUrl = config.baseUrl
|
||||
this.headers = {
|
||||
"Content-Type": "application/json",
|
||||
...config.defaultHeaders,
|
||||
...config.headers,
|
||||
}
|
||||
this.maxRetries = config.maxRetries ?? 3
|
||||
this.initialRetryDelay = config.initialRetryDelay ?? 500
|
||||
}
|
||||
|
||||
private async resolveHeaders(): Promise<Record<string, string>> {
|
||||
const resolvedHeaders: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(this.headers)) {
|
||||
const resolvedValue = await value
|
||||
if (resolvedValue !== null) {
|
||||
resolvedHeaders[key] = resolvedValue
|
||||
}
|
||||
}
|
||||
return resolvedHeaders
|
||||
}
|
||||
|
||||
private async post<ReqBody, ResBody>(
|
||||
url: string,
|
||||
data: ReqBody,
|
||||
options: FetchOptions,
|
||||
attempt: number,
|
||||
): Promise<ResBody | null> {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: await this.resolveHeaders(),
|
||||
body: JSON.stringify(data ?? {}),
|
||||
keepalive: true,
|
||||
...options,
|
||||
})
|
||||
|
||||
if (response.status === 401) return null
|
||||
|
||||
if (response.status !== 200 && response.status !== 202) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const responseText = await response.text()
|
||||
return responseText ? JSON.parse(responseText) : null
|
||||
} catch (error) {
|
||||
if (attempt < this.maxRetries) {
|
||||
const delay = this.initialRetryDelay * 2 ** attempt
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
return this.post<ReqBody, ResBody>(url, data, options, attempt + 1)
|
||||
}
|
||||
console.error("Max retries reached:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async fetch<ReqBody, ResBody>(
|
||||
path: string,
|
||||
data: ReqBody,
|
||||
options: FetchOptions = {},
|
||||
): Promise<ResBody | null> {
|
||||
const url = `${this.baseUrl}${path}`
|
||||
return this.post<ReqBody, ResBody>(url, data, options, 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
import { Api } from "./api"
|
||||
|
||||
export type TrackHandlerPayload =
|
||||
| {
|
||||
type: "track"
|
||||
payload: TrackPayload
|
||||
}
|
||||
| {
|
||||
type: "increment"
|
||||
payload: IncrementPayload
|
||||
}
|
||||
| {
|
||||
type: "decrement"
|
||||
payload: DecrementPayload
|
||||
}
|
||||
| {
|
||||
type: "alias"
|
||||
payload: AliasPayload
|
||||
}
|
||||
| {
|
||||
type: "identify"
|
||||
payload: IdentifyPayload
|
||||
}
|
||||
|
||||
export type TrackPayload = {
|
||||
name: string
|
||||
properties?: Record<string, unknown>
|
||||
profileId?: string
|
||||
}
|
||||
|
||||
export type TrackProperties = {
|
||||
[key: string]: unknown
|
||||
profileId?: string
|
||||
}
|
||||
|
||||
export type IdentifyPayload = {
|
||||
profileId: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email?: string
|
||||
avatar?: string
|
||||
properties?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type AliasPayload = {
|
||||
profileId: string
|
||||
alias: string
|
||||
}
|
||||
|
||||
export type IncrementPayload = {
|
||||
profileId: string
|
||||
property: string
|
||||
value?: number
|
||||
}
|
||||
|
||||
export type DecrementPayload = {
|
||||
profileId: string
|
||||
property: string
|
||||
value?: number
|
||||
}
|
||||
|
||||
export type OpenPanelOptions = {
|
||||
clientId: string
|
||||
clientSecret?: string
|
||||
apiUrl?: string
|
||||
sdk?: string
|
||||
sdkVersion?: string
|
||||
waitForProfile?: boolean
|
||||
filter?: (payload: TrackHandlerPayload) => boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export class OpenPanel {
|
||||
api: Api
|
||||
profileId?: string
|
||||
global?: Record<string, unknown>
|
||||
queue: TrackHandlerPayload[] = []
|
||||
|
||||
constructor(public options: OpenPanelOptions) {
|
||||
const defaultHeaders: Record<string, string> = {
|
||||
"openpanel-client-id": options.clientId,
|
||||
}
|
||||
|
||||
if (options.clientSecret) {
|
||||
defaultHeaders["openpanel-client-secret"] = options.clientSecret
|
||||
}
|
||||
|
||||
defaultHeaders["openpanel-sdk-name"] = options.sdk || "node"
|
||||
defaultHeaders["openpanel-sdk-version"] = options.sdkVersion || process.env.SDK_VERSION!
|
||||
|
||||
this.api = new Api({
|
||||
baseUrl: options.apiUrl || "https://api.openpanel.dev",
|
||||
defaultHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
ready() {
|
||||
this.options.waitForProfile = false
|
||||
this.flush()
|
||||
}
|
||||
|
||||
async send(payload: TrackHandlerPayload) {
|
||||
if (this.options.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
||||
if (this.options.filter && !this.options.filter(payload)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.options.waitForProfile && !this.profileId) {
|
||||
this.queue.push(payload)
|
||||
return
|
||||
}
|
||||
return this.api.fetch("/track", payload)
|
||||
}
|
||||
|
||||
setGlobalProperties(properties: Record<string, unknown>) {
|
||||
this.global = {
|
||||
...this.global,
|
||||
...properties,
|
||||
}
|
||||
}
|
||||
|
||||
async track(name: string, properties?: TrackProperties) {
|
||||
return this.send({
|
||||
type: "track",
|
||||
payload: {
|
||||
name,
|
||||
profileId: properties?.profileId ?? this.profileId,
|
||||
properties: {
|
||||
...this.global,
|
||||
...properties,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async identify(payload: IdentifyPayload) {
|
||||
if (payload.profileId) {
|
||||
this.profileId = payload.profileId
|
||||
this.flush()
|
||||
}
|
||||
|
||||
if (Object.keys(payload).length > 1) {
|
||||
return this.send({
|
||||
type: "identify",
|
||||
payload: {
|
||||
...payload,
|
||||
properties: {
|
||||
...this.global,
|
||||
...payload.properties,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async alias(payload: AliasPayload) {
|
||||
return this.send({
|
||||
type: "alias",
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
async increment(payload: IncrementPayload) {
|
||||
return this.send({
|
||||
type: "increment",
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
async decrement(payload: DecrementPayload) {
|
||||
return this.send({
|
||||
type: "decrement",
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.profileId = undefined
|
||||
// should we force a session end here?
|
||||
}
|
||||
|
||||
flush() {
|
||||
this.queue.forEach((item) => {
|
||||
this.send({
|
||||
...item,
|
||||
// Not sure why ts-expect-error is needed here
|
||||
// @ts-expect-error
|
||||
payload: {
|
||||
...item.payload,
|
||||
profileId: item.payload.profileId ?? this.profileId,
|
||||
},
|
||||
})
|
||||
})
|
||||
this.queue = []
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
import type { OpenPanel } from "./op"
|
||||
|
||||
type Tracker = (name: string, properties?: Record<string, unknown>) => Promise<any>
|
||||
class TrackManager {
|
||||
private trackFns: Tracker[] = []
|
||||
|
||||
setTrackFn(fn: Tracker) {
|
||||
this.trackFns.push(fn)
|
||||
|
||||
return () => {
|
||||
this.trackFns = this.trackFns.filter((t) => t !== fn)
|
||||
}
|
||||
}
|
||||
|
||||
getTrackFn(): Tracker {
|
||||
if (this.trackFns.length === 0) {
|
||||
console.error("[Tracker warn]: Track function not set")
|
||||
}
|
||||
return (name, properties) => {
|
||||
return Promise.all(this.trackFns.map((fn) => fn(name, properties)))
|
||||
}
|
||||
}
|
||||
|
||||
setOpenPanelTracker(tracker: OpenPanel["track"]) {
|
||||
this.trackFns.push((name, properties) => tracker(name, properties))
|
||||
}
|
||||
}
|
||||
|
||||
export const trackManager = new TrackManager()
|
||||
export enum TrackerMapper {
|
||||
Identify = 1000,
|
||||
UserLogin = 1001,
|
||||
UiRenderInit = 1002,
|
||||
AppInit = 1003,
|
||||
// Biz
|
||||
NavigateEntry = 2000,
|
||||
BoostSent = 2001,
|
||||
Integration = 2002,
|
||||
DailyReportModal = 2003,
|
||||
SwitchToMasonry = 2004,
|
||||
WideMode = 2005,
|
||||
EntryContentHeaderImageGalleryClick = 2006,
|
||||
SearchOpen = 2007,
|
||||
QuickAddFeed = 2008,
|
||||
PlayerOpenDuration = 2009,
|
||||
UpdateRestart = 2010,
|
||||
TipModalOpened = 2011,
|
||||
FeedClaimed = 2012,
|
||||
DailyRewardClaimed = 2013,
|
||||
TipSent = 2014,
|
||||
}
|
||||
|
||||
const CodeToTrackerName = Object.fromEntries(
|
||||
Object.entries(TrackerMapper).map(([key, value]) => [value, key]),
|
||||
) as Record<number, string>
|
||||
|
||||
export class TrackerPoints {
|
||||
// App
|
||||
identify(userId: string) {
|
||||
this.track(TrackerMapper.Identify, { userId })
|
||||
}
|
||||
|
||||
appInit(props: {
|
||||
electron?: boolean
|
||||
rn?: boolean
|
||||
loading_time?: number
|
||||
using_indexed_db?: boolean
|
||||
data_hydrated_time?: number
|
||||
version?: string
|
||||
}) {
|
||||
this.track(TrackerMapper.AppInit, props)
|
||||
}
|
||||
|
||||
userLogin(userId: string) {
|
||||
this.track(TrackerMapper.UserLogin, { userId })
|
||||
}
|
||||
|
||||
/**
|
||||
* For desktop UI only
|
||||
*/
|
||||
uiRenderInit(spentTime: number) {
|
||||
this.track(TrackerMapper.UiRenderInit, { time: spentTime })
|
||||
}
|
||||
|
||||
navigateEntry(props: { feedId?: string; entryId?: string; timelineId?: string }) {
|
||||
this.track(TrackerMapper.NavigateEntry, props)
|
||||
}
|
||||
boostSent(props: { amount: string; feedId: string }) {
|
||||
this.track(TrackerMapper.BoostSent, props)
|
||||
}
|
||||
|
||||
integration(props: { type: string; event: string }) {
|
||||
this.track(TrackerMapper.Integration, props)
|
||||
}
|
||||
|
||||
dailyReportModal() {
|
||||
this.track(TrackerMapper.DailyReportModal)
|
||||
}
|
||||
|
||||
switchToMasonry() {
|
||||
this.track(TrackerMapper.SwitchToMasonry)
|
||||
}
|
||||
|
||||
wideMode(props: { mode: "wide" | "normal" }) {
|
||||
this.track(TrackerMapper.WideMode, props)
|
||||
}
|
||||
|
||||
entryContentHeaderImageGalleryClick(props: { feedId?: string }) {
|
||||
this.track(TrackerMapper.EntryContentHeaderImageGalleryClick, props)
|
||||
}
|
||||
searchOpen() {
|
||||
this.track(TrackerMapper.SearchOpen)
|
||||
}
|
||||
|
||||
quickAddFeed(props: { type: "url" | "search"; defaultView: number }) {
|
||||
this.track(TrackerMapper.QuickAddFeed, props)
|
||||
}
|
||||
playerOpenDuration(props: {
|
||||
duration: number
|
||||
status?: "playing" | "loading" | "paused"
|
||||
trigger?: "manual" | "beforeunload"
|
||||
}) {
|
||||
this.track(TrackerMapper.PlayerOpenDuration, props)
|
||||
}
|
||||
|
||||
updateRestart(props: { type: "app" | "renderer" | "pwa" }) {
|
||||
this.track(TrackerMapper.UpdateRestart, props)
|
||||
}
|
||||
|
||||
tipModalOpened(props: { entryId?: string }) {
|
||||
this.track(TrackerMapper.TipModalOpened, props)
|
||||
}
|
||||
|
||||
feedClaimed(props: { feedId: string }) {
|
||||
this.track(TrackerMapper.FeedClaimed, props)
|
||||
}
|
||||
|
||||
dailyRewardClaimed() {
|
||||
this.track(TrackerMapper.DailyRewardClaimed)
|
||||
}
|
||||
|
||||
tipSent(props: { amount: string; entryId: string }) {
|
||||
this.track(TrackerMapper.TipSent, props)
|
||||
}
|
||||
|
||||
private track(code: TrackerMapper, properties?: Record<string, unknown>) {
|
||||
if (code) {
|
||||
let name = CodeToTrackerName[code]
|
||||
if (name) {
|
||||
name = snakeCase(name)
|
||||
Reflect.apply(trackManager.getTrackFn(), null, [name, { ...properties, __code: code }])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const snakeCase = (string: string) => {
|
||||
return string
|
||||
.replaceAll(/\W+/g, " ")
|
||||
.split(/ |\B(?=[A-Z])/)
|
||||
.map((word) => word.toLowerCase())
|
||||
.join("_")
|
||||
}
|
||||
|
||||
export type AllTrackers = keyof TrackerPoints
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../tsconfig.extend.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"declaration": false
|
||||
}
|
||||
}
|
||||
|
|
@ -481,6 +481,9 @@ importers:
|
|||
'@follow/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/shared
|
||||
'@follow/tracker':
|
||||
specifier: workspace:*
|
||||
version: link:../../../../packages/tracker
|
||||
'@fontsource/sn-pro':
|
||||
specifier: 5.2.5
|
||||
version: 5.2.5
|
||||
|
|
@ -788,6 +791,9 @@ importers:
|
|||
'@hookform/resolvers':
|
||||
specifier: 4.1.3
|
||||
version: 4.1.3(react-hook-form@7.54.2(react@18.3.1))
|
||||
'@openpanel/sdk':
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
'@react-native-community/image-editor':
|
||||
specifier: 4.3.0
|
||||
version: 4.3.0(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))
|
||||
|
|
@ -842,6 +848,9 @@ importers:
|
|||
expo-apple-authentication:
|
||||
specifier: 7.1.3
|
||||
version: 7.1.3(expo@52.0.39(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.13.4(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)(utf-8-validate@6.0.5))(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))
|
||||
expo-application:
|
||||
specifier: 6.0.2
|
||||
version: 6.0.2(expo@52.0.39(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.13.4(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)(utf-8-validate@6.0.5))
|
||||
expo-av:
|
||||
specifier: 15.0.2
|
||||
version: 15.0.2(1c0838ca83e190af6509d6d21281a8a6)
|
||||
|
|
@ -1536,6 +1545,8 @@ importers:
|
|||
specifier: 3.24.2
|
||||
version: 3.24.2
|
||||
|
||||
packages/tracker: {}
|
||||
|
||||
packages/types: {}
|
||||
|
||||
packages/utils:
|
||||
|
|
@ -9153,6 +9164,11 @@ packages:
|
|||
expo: '*'
|
||||
react-native: '*'
|
||||
|
||||
expo-application@6.0.2:
|
||||
resolution: {integrity: sha512-qcj6kGq3mc7x5yIb5KxESurFTJCoEKwNEL34RdPEvTB/xhl7SeVZlu05sZBqxB1V4Ryzq/LsCb7NHNfBbb3L7A==}
|
||||
peerDependencies:
|
||||
expo: '*'
|
||||
|
||||
expo-asset@11.0.5:
|
||||
resolution: {integrity: sha512-TL60LmMBGVzs3NQcO8ylWqBumMh4sx0lmeJsn7+9C88fylGDhyyVnKZ1PyTXo9CVDBkndutZx2JUEQWM9BaiXw==}
|
||||
peerDependencies:
|
||||
|
|
@ -25941,6 +25957,10 @@ snapshots:
|
|||
expo: 52.0.39(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.13.4(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)(utf-8-validate@6.0.5)
|
||||
react-native: 0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)
|
||||
|
||||
expo-application@6.0.2(expo@52.0.39(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.13.4(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)(utf-8-validate@6.0.5)):
|
||||
dependencies:
|
||||
expo: 52.0.39(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.13.4(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)(utf-8-validate@6.0.5)
|
||||
|
||||
expo-asset@11.0.5(expo@52.0.35(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@expo/metro-runtime@4.0.1(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5)))(bufferutil@4.0.9)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.13.4(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1))(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1)(utf-8-validate@6.0.5))(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@types/react@18.3.12)(bufferutil@4.0.9)(react@18.3.1)(utf-8-validate@6.0.5))(react@18.3.1):
|
||||
dependencies:
|
||||
'@expo/image-utils': 0.6.5
|
||||
|
|
|
|||
Loading…
Reference in New Issue