From d122eb3f5045ac7f4dcb43b0e5f33c1e3753c7ee Mon Sep 17 00:00:00 2001 From: xudaotutou <13435638964@163.com> Date: Thu, 13 Feb 2025 15:06:19 +0800 Subject: [PATCH] feat(desktop): add umami (#5346) feat(destkop):add umami --- frontend/desktop/package.json | 2 + frontend/desktop/src/api/auth.ts | 5 +- .../src/components/team/WorkspaceToggle.tsx | 4 +- frontend/desktop/src/constants/account.ts | 5 ++ frontend/desktop/src/pages/_app.tsx | 1 + frontend/desktop/src/pages/_document.tsx | 4 +- .../src/pages/api/platform/getAppConfig.ts | 36 +++++++---- .../src/pages/api/platform/getCommonConfig.ts | 3 +- frontend/desktop/src/pages/index.tsx | 36 ++++++++++- .../src/services/backend/globalAuth.ts | 64 ++++++++++++------- .../src/services/backend/tracking/index.ts | 31 +++++++++ frontend/desktop/src/services/enable.ts | 1 + frontend/desktop/src/stores/config.ts | 11 ++-- frontend/desktop/src/stores/session.ts | 9 +++ frontend/desktop/src/types/index.ts | 14 ++-- frontend/desktop/src/types/system.ts | 15 ++++- frontend/pnpm-lock.yaml | 14 ++++ 17 files changed, 195 insertions(+), 60 deletions(-) create mode 100644 frontend/desktop/src/services/backend/tracking/index.ts diff --git a/frontend/desktop/package.json b/frontend/desktop/package.json index ca57c7a6f5a..5a3379fb9d6 100644 --- a/frontend/desktop/package.json +++ b/frontend/desktop/package.json @@ -34,6 +34,7 @@ "@sealos/driver": "workspace:^", "@sealos/ui": "workspace:^", "@tanstack/react-query": "^4.35.3", + "@umami/node": "^0.4.0", "axios": "^1.5.1", "clsx": "^1.2.1", "cors": "^2.8.5", @@ -91,6 +92,7 @@ "@types/nprogress": "^0.2.1", "@types/react": "18.2.37", "@types/react-dom": "18.0.11", + "@types/umami-browser": "^2.3.2", "@types/uuid": "^9.0.4", "dotenv-cli": "^7.3.0", "jest": "^29.7.0", diff --git a/frontend/desktop/src/api/auth.ts b/frontend/desktop/src/api/auth.ts index d09ca3c84ae..65846891455 100644 --- a/frontend/desktop/src/api/auth.ts +++ b/frontend/desktop/src/api/auth.ts @@ -208,13 +208,16 @@ export const _enterpriseRealNameAuthCancelRequest = (request: AxiosInstance) => export const _getAmount = (request: AxiosInstance) => () => request>('/api/account/getAmount'); - +export const _verifyToken = (request: AxiosInstance) => () => + request>('/api/auth/verify'); export const passwordExistRequest = _passwordExistRequest(request); export const passwordLoginRequest = _passwordLoginRequest(request, (token) => { useSessionStore.setState({ token }); }); + export const passwordModifyRequest = _passwordModifyRequest(request); export const UserInfo = _UserInfo(request); +export const verifyToken = _verifyToken(request); export const regionList = _regionList(request); export const getSmsBindCodeRequest = _getSmsBindCodeRequest(request); diff --git a/frontend/desktop/src/components/team/WorkspaceToggle.tsx b/frontend/desktop/src/components/team/WorkspaceToggle.tsx index c9b02ef69bf..81136db5346 100644 --- a/frontend/desktop/src/components/team/WorkspaceToggle.tsx +++ b/frontend/desktop/src/components/team/WorkspaceToggle.tsx @@ -1,7 +1,6 @@ import { nsListRequest, switchRequest } from '@/api/namespace'; import NsListItem from '@/components/team/NsListItem'; import TeamCenter from '@/components/team/TeamCenter'; -import useAppStore from '@/stores/app'; import useSessionStore from '@/stores/session'; import { NSType } from '@/types/team'; import { AccessTokenPayload } from '@/types/token'; @@ -16,13 +15,12 @@ import { CubeIcon, DesktopExchangeIcon } from '../icons'; export default function WorkspaceToggle() { const disclosure = useDisclosure(); - const { setWorkSpaceId, session } = useSessionStore(); + const { session } = useSessionStore(); const { t } = useTranslation(); const user = session?.user; const ns_uid = user?.ns_uid || ''; const router = useRouter(); const queryClient = useQueryClient(); - const { init } = useAppStore(); const mutation = useMutation({ mutationFn: switchRequest, async onSuccess(data) { diff --git a/frontend/desktop/src/constants/account.ts b/frontend/desktop/src/constants/account.ts index 0ccc4f735e7..c41c44de088 100644 --- a/frontend/desktop/src/constants/account.ts +++ b/frontend/desktop/src/constants/account.ts @@ -4,3 +4,8 @@ export const LicenseFrontendKey = 'cloud.sealos.io/license-frontend'; export const templateDeployKey = 'cloud.sealos.io/deploy-on-sealos'; export const userSystemNamespace = 'user-system' as const; + +export enum trackEventName { + 'dailyLoginFirst' = 'dailyLoginFirst', + 'signUp' = 'signUp' +} diff --git a/frontend/desktop/src/pages/_app.tsx b/frontend/desktop/src/pages/_app.tsx index 2205a6583b4..a5847777351 100644 --- a/frontend/desktop/src/pages/_app.tsx +++ b/frontend/desktop/src/pages/_app.tsx @@ -48,4 +48,5 @@ const App = ({ Component, pageProps }: AppProps) => { ); }; + export default appWithTranslation(App); diff --git a/frontend/desktop/src/pages/_document.tsx b/frontend/desktop/src/pages/_document.tsx index 91869f08cfe..1a1c9f10283 100644 --- a/frontend/desktop/src/pages/_document.tsx +++ b/frontend/desktop/src/pages/_document.tsx @@ -1,6 +1,6 @@ -import { Head, Html, Main, NextScript } from 'next/document'; -import { ColorModeScript } from '@chakra-ui/react'; import { theme } from '@/styles/chakraTheme'; +import { ColorModeScript } from '@chakra-ui/react'; +import { Head, Html, Main, NextScript } from 'next/document'; export default function Document() { return ( diff --git a/frontend/desktop/src/pages/api/platform/getAppConfig.ts b/frontend/desktop/src/pages/api/platform/getAppConfig.ts index 1c74133ba50..617959d8f2f 100644 --- a/frontend/desktop/src/pages/api/platform/getAppConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getAppConfig.ts @@ -1,23 +1,24 @@ +import { getAuthClientConfig } from '@/pages/api/platform/getAuthConfig'; +import { getCloudConfig } from '@/pages/api/platform/getCloudConfig'; +import { getLayoutConfig } from '@/pages/api/platform/getLayoutConfig'; +import { + commitTransactionjob, + finishTransactionJob, + runTransactionjob +} from '@/services/backend/cronjob'; import { jsonRes } from '@/services/backend/response'; -import type { NextApiRequest, NextApiResponse } from 'next'; import { AppClientConfigType, AuthClientConfigType, CloudConfigType, CommonClientConfigType, DefaultAppClientConfig, - LayoutConfigType + LayoutConfigType, + TrackingConfigType } from '@/types/system'; -import { getCloudConfig } from '@/pages/api/platform/getCloudConfig'; -import { getAuthClientConfig } from '@/pages/api/platform/getAuthConfig'; -import { getLayoutConfig } from '@/pages/api/platform/getLayoutConfig'; -import { getCommonClientConfig } from './getCommonConfig'; import { Cron } from 'croner'; -import { - commitTransactionjob, - finishTransactionJob, - runTransactionjob -} from '@/services/backend/cronjob'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getCommonClientConfig } from './getCommonConfig'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const config = await getAppConfig(); @@ -31,7 +32,8 @@ function genResConfig( cloudConf: CloudConfigType, authConf: AuthClientConfigType, commonConf: CommonClientConfigType, - layoutConf: LayoutConfigType + layoutConf: LayoutConfigType, + tracking: Required ): AppClientConfigType { return { cloud: cloudConf, @@ -39,7 +41,8 @@ function genResConfig( desktop: { auth: authConf, layout: layoutConf - } + }, + tracking: tracking }; } @@ -49,7 +52,12 @@ export async function getAppConfig(): Promise { const authConf = await getAuthClientConfig(); const commonConf = await getCommonClientConfig(); const layoutConf = await getLayoutConfig(); - const conf = genResConfig(cloudConf, authConf, commonConf, layoutConf); + const _tracking = global.AppConfig.tracking; + const tracking: Required = { + websiteId: _tracking.websiteId || '', + hostUrl: _tracking.hostUrl || '' + }; + const conf = genResConfig(cloudConf, authConf, commonConf, layoutConf, tracking); if (!global.commitCroner) { // console.log('init commit croner'); global.commitCroner = new Cron('* * * * * *', commitTransactionjob, { diff --git a/frontend/desktop/src/pages/api/platform/getCommonConfig.ts b/frontend/desktop/src/pages/api/platform/getCommonConfig.ts index 517b1cba346..c34c82b3308 100644 --- a/frontend/desktop/src/pages/api/platform/getCommonConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getCommonConfig.ts @@ -6,8 +6,8 @@ import { DefaultCommonClientConfig } from '@/types/system'; import { readFileSync } from 'fs'; -import type { NextApiRequest, NextApiResponse } from 'next'; import yaml from 'js-yaml'; +import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const config = await getCommonClientConfig(); @@ -18,6 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } function genResCommonClientConfig(common: CommonConfigType): CommonClientConfigType { return { + trackingEnabled: !!common.trackingEnabled, enterpriseRealNameAuthEnabled: !!common.enterpriseRealNameAuthEnabled, realNameAuthEnabled: !!common.realNameAuthEnabled, realNameReward: common.realNameReward || 0, diff --git a/frontend/desktop/src/pages/index.tsx b/frontend/desktop/src/pages/index.tsx index 6d74c5787c4..8c375187ef7 100644 --- a/frontend/desktop/src/pages/index.tsx +++ b/frontend/desktop/src/pages/index.tsx @@ -1,5 +1,6 @@ import { nsListRequest, switchRequest } from '@/api/namespace'; import DesktopContent from '@/components/desktop_content'; +import { trackEventName } from '@/constants/account'; import useAppStore from '@/stores/app'; import useCallbackStore from '@/stores/callback'; import { useConfigStore } from '@/stores/config'; @@ -14,6 +15,7 @@ import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; import { compareFirstLanguages } from '@/utils/tools'; import { Box, useColorMode } from '@chakra-ui/react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import dayjs from 'dayjs'; import { jwtDecode } from 'jwt-decode'; import { isString } from 'lodash'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; @@ -32,13 +34,13 @@ export const MoreAppsContext = createContext(null); export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: string }) { const router = useRouter(); - const { isUserLogin } = useSessionStore(); + const { firstUse, setFirstUse, isUserLogin } = useSessionStore(); const { colorMode, toggleColorMode } = useColorMode(); const init = useAppStore((state) => state.init); const setAutoLaunch = useAppStore((state) => state.setAutoLaunch); const { autolaunchWorkspaceUid } = useAppStore(); const { session } = useSessionStore(); - const { layoutConfig } = useConfigStore(); + const { layoutConfig, commonConfig, trackingConfig } = useConfigStore(); const { workspaceInviteCode, setWorkspaceInviteCode } = useCallbackStore(); const { setCanShowGuide } = useDesktopConfigStore(); @@ -78,6 +80,9 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str const is_login = isUserLogin(); const whitelistApps = ['system-template', 'system-fastdeploy']; if (!is_login) { + // clear firstusetime + setFirstUse(null); + const { appkey, appQuery } = parseOpenappQuery((query?.openapp as string) || ''); // Invited new user if (query?.uid && typeof query?.uid === 'string') { @@ -95,6 +100,7 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str if (isString(query?.workspaceUid)) workspaceUid = query.workspaceUid; if (appkey && typeof appQuery === 'string') setAutoLaunch(appkey, { raw: appQuery }, workspaceUid); + router.replace(destination); } else { let workspaceUid: string | undefined; @@ -205,7 +211,31 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str return; } }, [workspaceInviteCode]); - + useEffect(() => { + (async (state) => { + try { + if ( + commonConfig?.trackingEnabled && + (!firstUse || !dayjs(firstUse).isSame(dayjs(), 'day')) + ) { + const umami = window.umami; + if (!!umami) { + const result = await umami.track(trackEventName.dailyLoginFirst, { + userId: session?.user.userId!, + userUid: session?.user.userUid! + }); + if (result.ok && result.status === 200) { + setFirstUse(new Date()); + } else { + console.error('Failed to update first use date'); + } + } + } + } catch (e) { + console.log(e); + } + })(); + }, [commonConfig, firstUse]); return ( diff --git a/frontend/desktop/src/services/backend/globalAuth.ts b/frontend/desktop/src/services/backend/globalAuth.ts index 93790424131..0ce532ce98f 100644 --- a/frontend/desktop/src/services/backend/globalAuth.ts +++ b/frontend/desktop/src/services/backend/globalAuth.ts @@ -12,7 +12,8 @@ import { User, UserStatus } from 'prisma/global/generated/client'; -import { enableSignUp } from '../enable'; +import { enableSignUp, enableTracking } from '../enable'; +import { trackSignUp } from './tracking'; type TransactionClient = Omit< PrismaClient, @@ -254,7 +255,6 @@ export async function signUpByPassword({ return { user }; }); - return result; } catch (error) { console.error('globalAuth: Error during sign up:', error); @@ -329,13 +329,21 @@ export const getGlobalToken = async ({ password, semData }); - result && (user = result.user); - if (inviterId && result) { - inviteHandler({ - inviterId: inviterId, - inviteeId: result?.user.name, - signResult: result - }); + if (!!result) { + user = result.user; + if (inviterId && result) { + inviteHandler({ + inviterId: inviterId, + inviteeId: result?.user.name, + signResult: result + }); + } + if (enableTracking()) { + await trackSignUp({ + userId: result.user.id, + userUid: result.user.uid + }); + } } } else { const result = await signInByPassword({ @@ -356,22 +364,30 @@ export const getGlobalToken = async ({ avatar_url, semData }); - result && (user = result.user); - if (inviterId && result) { - inviteHandler({ - inviterId: inviterId, - inviteeId: result?.user.name, - signResult: result - }); - } - if (bdVid && result) { - uploadConvertData({ newType: [3], bdVid }) - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.log(err); + if (result) { + user = result.user; + if (inviterId) { + inviteHandler({ + inviterId: inviterId, + inviteeId: result?.user.name, + signResult: result }); + } + if (bdVid) { + await uploadConvertData({ newType: [3], bdVid }) + .then((res) => { + console.log(res); + }) + .catch((err) => { + console.log(err); + }); + } + if (enableTracking()) { + await trackSignUp({ + userId: result.user.id, + userUid: result.user.uid + }); + } } } else { const result = await signIn({ diff --git a/frontend/desktop/src/services/backend/tracking/index.ts b/frontend/desktop/src/services/backend/tracking/index.ts new file mode 100644 index 00000000000..edb29b5d5f9 --- /dev/null +++ b/frontend/desktop/src/services/backend/tracking/index.ts @@ -0,0 +1,31 @@ +import { trackEventName } from '@/constants/account'; +import { Umami } from '@umami/node'; +const getUmami = () => { + return new Umami({ + websiteId: global.AppConfig.tracking.websiteId, + hostUrl: global.AppConfig.tracking.hostUrl + }); +}; +export type TLoginPayload = { + userUid: string; + userId: string; +}; +export const trackSignUp = (data: TLoginPayload) => { + const umami = getUmami(); + return umami + .track(trackEventName.signUp, data) + .then((res) => { + console.log('[tracking][signUp][success]'); + }) + .catch((e) => { + console.error('[tracking][signUp]:', e); + return Promise.resolve(null); + }); +}; +export const trackDailyLoginFirst = (data: TLoginPayload) => { + const umami = getUmami(); + return umami.track(trackEventName.dailyLoginFirst, data).catch((e) => { + console.error('[tracking][dailyLoginFirst]:', e); + return Promise.resolve(null); + }); +}; diff --git a/frontend/desktop/src/services/enable.ts b/frontend/desktop/src/services/enable.ts index 4cd4da93813..4eb2b97ab63 100644 --- a/frontend/desktop/src/services/enable.ts +++ b/frontend/desktop/src/services/enable.ts @@ -18,6 +18,7 @@ export const getBillingUrl = () => global.AppConfig.desktop.auth.billingUrl || ' export const getWorkorderUrl = () => global.AppConfig.desktop.auth.workorderUrl || ''; export const getCvmUrl = () => global.AppConfig.desktop.auth.cloudVitrualMachineUrl || ''; export const getTeamLimit = () => global.AppConfig.desktop.teamManagement?.maxTeamCount || 50; +export const enableTracking = () => !!global.AppConfig.common.trackingEnabled; export const getTeamInviteLimit = () => global.AppConfig.desktop.teamManagement?.maxTeamMemberCount || 50; diff --git a/frontend/desktop/src/stores/config.ts b/frontend/desktop/src/stores/config.ts index be7a147fba9..951a9653b60 100644 --- a/frontend/desktop/src/stores/config.ts +++ b/frontend/desktop/src/stores/config.ts @@ -1,19 +1,21 @@ +import { getAppConfig } from '@/api/platform'; import { + AuthClientConfigType, CloudConfigType, + CommonClientConfigType, LayoutConfigType, - AuthClientConfigType, - CommonClientConfigType + TrackingConfigType } from '@/types'; import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; -import { getAppConfig } from '@/api/platform'; type State = { cloudConfig?: CloudConfigType; authConfig?: AuthClientConfigType; commonConfig?: CommonClientConfigType; layoutConfig?: LayoutConfigType; + trackingConfig?: TrackingConfigType; initAppConfig: () => Promise; }; @@ -24,11 +26,12 @@ export const useConfigStore = create()( authConfig: undefined, commonConfig: undefined, layoutConfig: undefined, - + trackingConfig: undefined, async initAppConfig() { const data = await getAppConfig(); console.log('initAppConfig', data.data); set((state) => { + state.trackingConfig = data.data.tracking; state.layoutConfig = data.data.desktop.layout; state.authConfig = data.data.desktop.auth; state.cloudConfig = data.data.cloud; diff --git a/frontend/desktop/src/stores/session.ts b/frontend/desktop/src/stores/session.ts index 051e7c50ad7..064e7127c0a 100644 --- a/frontend/desktop/src/stores/session.ts +++ b/frontend/desktop/src/stores/session.ts @@ -13,9 +13,11 @@ type SessionState = { token: string; provider?: OauthProvider; oauth_state: string; + firstUse: Date | null; setSession: (ss: Session) => void; setSessionProp: (key: T, value: Session[T]) => void; delSession: () => void; + setFirstUse: (d: Date | null) => void; isUserLogin: () => boolean; /* when proxy oauth2.0 ,the domainState need to be used @@ -26,6 +28,7 @@ type SessionState = { action: string; statePayload: string[]; }; + setProvider: (provider?: OauthProvider) => void; setToken: (token: string) => void; lastWorkSpaceId: string; @@ -37,9 +40,15 @@ const useSessionStore = create()( immer((set, get) => ({ session: undefined, provider: undefined, + firstUse: null, oauth_state: '', token: '', lastWorkSpaceId: '', + setFirstUse(d) { + set({ + firstUse: d + }); + }, setSession: (ss: Session) => set({ session: ss }), setSessionProp: (key: keyof Session, value: any) => { set((state) => { diff --git a/frontend/desktop/src/types/index.ts b/frontend/desktop/src/types/index.ts index deff17a71ee..5746e373997 100644 --- a/frontend/desktop/src/types/index.ts +++ b/frontend/desktop/src/types/index.ts @@ -1,18 +1,19 @@ -import { type MongoClient } from 'mongodb'; -import { type AppConfigType } from './system'; +import { Umami } from '@umami/node'; import { Cron } from 'croner'; +import { type MongoClient } from 'mongodb'; import { Transporter } from 'nodemailer'; import SMTPPool from 'nodemailer/lib/smtp-pool'; +import { type AppConfigType } from './system'; export * from './api'; -export * from './session'; export * from './app'; export * from './crd'; +export * from './license'; +export * from './login'; export * from './payment'; +export * from './region'; +export * from './session'; export * from './system'; -export * from './login'; export * from './tools'; -export * from './license'; -export * from './region'; declare global { var mongodb: MongoClient | null; @@ -23,4 +24,5 @@ declare global { var WechatAccessToken: string | undefined; var WechatExpiresIn: number | undefined; var nodemailer: Transporter | undefined; + var umami: Umami | undefined; } diff --git a/frontend/desktop/src/types/system.ts b/frontend/desktop/src/types/system.ts index 91956b17cf5..e050cf3ec27 100644 --- a/frontend/desktop/src/types/system.ts +++ b/frontend/desktop/src/types/system.ts @@ -19,6 +19,7 @@ export type CommonConfigType = { objectstorageUrl: string; applaunchpadUrl: string; dbproviderUrl: string; + trackingEnabled: boolean; }; export type CommonClientConfigType = DeepRequired< Omit< @@ -63,7 +64,6 @@ export type LayoutConfigType = { customerServiceURL?: string; forcedLanguage?: string; currencySymbol?: 'shellCoin' | 'cny' | 'usd'; - protocol?: ProtocolConfigType; common: { githubStarEnabled: boolean; @@ -193,6 +193,11 @@ export type DesktopConfigType = { }; }; +export type TrackingConfigType = { + websiteId?: string; + hostUrl?: string; +}; + export type RealNameOSSConfigType = { accessKey: string; accessKeySecret: string; @@ -208,17 +213,20 @@ export type AppConfigType = { common: CommonConfigType; database: DatabaseConfigType; desktop: DesktopConfigType; + tracking: TrackingConfigType; realNameOSS: RealNameOSSConfigType; }; export type AppClientConfigType = { cloud: CloudConfigType; common: CommonClientConfigType; + tracking: Required; desktop: DesktopConfigType; }; export const DefaultCommonClientConfig: CommonClientConfigType = { enterpriseRealNameAuthEnabled: false, + trackingEnabled: false, realNameAuthEnabled: false, realNameReward: 0, guideEnabled: false, @@ -307,10 +315,13 @@ export const DefaultAuthClientConfig: AuthClientConfigType = { }, billingToken: '' }; - export const DefaultAppClientConfig: AppClientConfigType = { cloud: DefaultCloudConfig, common: DefaultCommonClientConfig, + tracking: { + websiteId: '', + hostUrl: '' + }, desktop: { layout: DefaultLayoutConfig, auth: DefaultAuthClientConfig diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 826cb7e235c..af30676f32b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -111,6 +111,9 @@ importers: '@tanstack/react-query': specifier: ^4.35.3 version: 4.36.1(react-dom@18.2.0)(react@18.2.0) + '@umami/node': + specifier: ^0.4.0 + version: 0.4.0 axios: specifier: ^1.5.1 version: 1.6.2 @@ -277,6 +280,9 @@ importers: '@types/react-dom': specifier: 18.0.11 version: 18.0.11 + '@types/umami-browser': + specifier: ^2.3.2 + version: 2.3.2 '@types/uuid': specifier: ^9.0.4 version: 9.0.7 @@ -10510,6 +10516,10 @@ packages: resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} dev: false + /@types/umami-browser@2.3.2: + resolution: {integrity: sha512-Y/6dm2EDSw57x3nxFz0X4WoK0ykeWKEGJnrKVGSXE6r6LlibPcKbVr5jPcmEfwya9l2PWzzIlOXRVH/7RU/dUQ==} + dev: true + /@types/unist@2.0.10: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} dev: false @@ -10866,6 +10876,10 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@umami/node@0.4.0: + resolution: {integrity: sha512-pyphprbiF7KiDSc+SWZ4/rVM8B5vU27zIiFfEPj2lEqczpI4xAKSp+dM3tlzyRAWJL32fcbCfAaLGhJZQV13Rg==} + dev: false + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true