Repository: open-condo-software/condo
Pull request #5868: feat(resident-app): INFRA-904 abstract analytics with groups and providers 📊
Author: login: SavelevMatthew
Labels: name: ✋🙂 Review please
description: Comments are resolved, take a look, please
name: 😎 Cool
name: 🐘 BIG
description: No so easy to review changes up to 1300 lines of code
Created: 2025-03-02T19:45:04Z | Merged: 2025-03-06T08:40:07Z



Diff:
diff --git a/.github/workflows/nodejs.condo.ci.yml b/.github/workflows/nodejs.condo.ci.yml
index 97664b95b8a..3e9f65ddeab 100644
--- a/.github/workflows/nodejs.condo.ci.yml
+++ b/.github/workflows/nodejs.condo.ci.yml
@@ -184,7 +184,7 @@ jobs:
           yarn workspaces focus root
       - name: Check JS code-style rules
         run: |
-          yarn lint:code
+          yarn lint:code --quiet
       - name: Check styling rules
         run: |
           yarn lint:styles
diff --git a/apps/resident-app b/apps/resident-app
index a2c3cbd4132..fe02aa0c4b2 160000
--- a/apps/resident-app
+++ b/apps/resident-app
@@ -1 +1 @@
-Subproject commit a2c3cbd41328d7c646d4d9a2c06cc979c393e992
+Subproject commit fe02aa0c4b2b40cb4b2d5954997c57f01891198e
diff --git a/packages/miniapp-utils/package.json b/packages/miniapp-utils/package.json
index 2938090c639..31eb319c9aa 100644
--- a/packages/miniapp-utils/package.json
+++ b/packages/miniapp-utils/package.json
@@ -28,7 +28,7 @@
     "build:deps": "turbo build --filter=@open-condo/miniapp-utils^...",
     "build:code": "NODE_ENV=production rollup -c",
     "build:types": "tsc --declaration --emitDeclarationOnly",
-    "build": "rimraf dist && yarn build:code && yarn build:types",
+    "build": "tsup",
     "dev": "rimraf dist && yarn build:deps &&(rollup -c -w & tsc --declaration --emitDeclarationOnly --watch)"
   },
   "files": [
@@ -37,82 +37,97 @@
   "typesVersions": {
     "*": {
       ".": [
-        "dist/types/index.d.ts"
+        "dist/index.d.ts"
+      ],
+      "helpers/posthog": [
+        "dist/helpers/posthog.d.ts"
+      ],
+      "helpers/analytics": [
+        "dist/helpers/analytics/index.d.ts"
       ],
       "helpers/apollo": [
-        "dist/types/helpers/apollo.d.ts"
+        "dist/helpers/apollo.d.ts"
       ],
       "helpers/collections": [
-        "dist/types/helpers/collections.d.ts"
+        "dist/helpers/collections.d.ts"
       ],
       "helpers/cookies": [
-        "dist/types/helpers/cookies.d.ts"
+        "dist/helpers/cookies.d.ts"
       ],
       "helpers/environment": [
-        "dist/types/helpers/environment.d.ts"
+        "dist/helpers/environment.d.ts"
       ],
       "helpers/sender": [
-        "dist/types/helpers/sender.d.ts"
+        "dist/helpers/sender.d.ts"
       ],
       "helpers/uuid": [
-        "dist/types/helpers/uuid.d.ts"
+        "dist/helpers/uuid.d.ts"
       ],
       "hooks/useEffectOnce": [
-        "dist/types/hooks/useEffectOnce.d.ts"
+        "dist/hooks/useEffectOnce.d.ts"
       ],
       "hooks/usePrevious": [
-        "dist/types/hooks/usePrevious.d.ts"
+        "dist/hooks/usePrevious.d.ts"
       ]
     }
   },
-  "types": "dist/types/index.d.ts",
-  "main": "dist/cjs/index.js",
-  "module": "/dist/esm/index.js",
+  "types": "dist/index.d.ts",
+  "main": "dist/index.js",
   "exports": {
     ".": {
-      "types": "./dist/types/index.d.ts",
-      "require": "./dist/cjs/index.js",
-      "import": "./dist/esm/index.js"
+      "types": "./dist/index.d.ts",
+      "require": "./dist/index.js",
+      "import": "./dist/index.mjs"
+    },
+    "./helpers/posthog": {
+      "types": "./dist/helpers/posthog.d.ts",
+      "require": "./dist/helpers/posthog.js",
+      "import": "./dist/helpers/posthog.mjs"
+    },
+    "./helpers/analytics": {
+      "types": "./dist/helpers/analytics/index.d.ts",
+      "require": "./dist/helpers/analytics/index.js",
+      "import": "./dist/helpers/analytics/index.mjs"
     },
     "./helpers/apollo": {
-      "types": "./dist/types/helpers/apollo.d.ts",
-      "require": "./dist/cjs/helpers/apollo.js",
-      "import": "./dist/esm/helpers/apollo.js"
+      "types": "./dist/helpers/apollo.d.ts",
+      "require": "./dist/helpers/apollo.js",
+      "import": "./dist/helpers/apollo.mjs"
     },
     "./helpers/collections": {
-      "types": "./dist/types/helpers/collections.d.ts",
-      "require": "./dist/cjs/helpers/collections.js",
-      "import": "./dist/esm/helpers/collections.js"
+      "types": "./dist/helpers/collections.d.ts",
+      "require": "./dist/helpers/collections.js",
+      "import": "./dist/helpers/collections.mjs"
     },
     "./helpers/cookies": {
-      "types": "./dist/types/helpers/cookies.d.ts",
-      "require": "./dist/cjs/helpers/cookies.js",
-      "import": "./dist/esm/helpers/cookies.js"
+      "types": "./dist/helpers/cookies.d.ts",
+      "require": "./dist/helpers/cookies.js",
+      "import": "./dist/helpers/cookies.mjs"
     },
     "./helpers/environment": {
-      "types": "./dist/types/helpers/environment.d.ts",
-      "require": "./dist/cjs/helpers/environment.js",
-      "import": "./dist/esm/helpers/environment.js"
+      "types": "./dist/helpers/environment.d.ts",
+      "require": "./dist/helpers/environment.js",
+      "import": "./dist/helpers/environment.mjs"
     },
     "./helpers/sender": {
-      "types": "./dist/types/helpers/sender.d.ts",
-      "require": "./dist/cjs/helpers/sender.js",
-      "import": "./dist/esm/helpers/sender.js"
+      "types": "./dist/helpers/sender.d.ts",
+      "require": "./dist/helpers/sender.js",
+      "import": "./dist/helpers/sender.mjs"
     },
     "./helpers/uuid": {
-      "types": "./dist/types/helpers/uuid.d.ts",
-      "require": "./dist/cjs/helpers/uuid.js",
-      "import": "./dist/esm/helpers/uuid.js"
+      "types": "./dist/helpers/uuid.d.ts",
+      "require": "./dist/helpers/uuid.js",
+      "import": "./dist/helpers/uuid.mjs"
     },
     "./hooks/useEffectOnce": {
-      "types": "./dist/types/hooks/useEffectOnce.d.ts",
-      "require": "./dist/cjs/hooks/useEffectOnce.js",
-      "import": "./dist/esm/hooks/useEffectOnce.js"
+      "types": "./dist/hooks/useEffectOnce.d.ts",
+      "require": "./dist/hooks/useEffectOnce.js",
+      "import": "./dist/hooks/useEffectOnce.mjs"
     },
     "./hooks/usePrevious": {
-      "types": "./dist/types/hooks/usePrevious.d.ts",
-      "require": "./dist/cjs/hooks/usePrevious.js",
-      "import": "./dist/esm/hooks/usePrevious.js"
+      "types": "./dist/hooks/usePrevious.d.ts",
+      "require": "./dist/hooks/usePrevious.js",
+      "import": "./dist/hooks/usePrevious.mjs"
     }
   },
   "publishConfig": {
@@ -120,27 +135,19 @@
   },
   "peerDependencies": {
     "@apollo/client": ">=3.0.0",
+    "analytics": "^0.8.16",
     "react": ">=16.0.0",
     "react-dom": ">=16.0.0"
   },
   "devDependencies": {
     "@apollo/client": "^3.11.8",
-    "@babel/preset-react": "^7.24.7",
     "@open-condo/tsconfig": "workspace:^",
-    "@rollup/plugin-babel": "^6.0.4",
-    "@rollup/plugin-commonjs": "^26.0.1",
-    "@rollup/plugin-node-resolve": "^15.2.3",
-    "@rollup/plugin-typescript": "^11.1.6",
     "@types/react": "^16.14.61",
-    "babel-core": "^6.26.3",
-    "babel-loader": "^9.1.3",
+    "analytics": "^0.8.16",
     "graphql": "^16.8.1",
     "react": "^16.14.0",
     "react-dom": "^16.14.0",
-    "rimraf": "^6.0.1",
-    "rollup": "^4.28.0",
-    "rollup-plugin-peer-deps-external": "^2.2.4",
-    "rollup-plugin-uglify": "^6.0.4",
+    "tsup": "^8.4.0",
     "typescript": "^5.5.4"
   },
   "dependencies": {
diff --git a/packages/miniapp-utils/rollup.config.mjs b/packages/miniapp-utils/rollup.config.mjs
deleted file mode 100644
index dac2ce13e3d..00000000000
--- a/packages/miniapp-utils/rollup.config.mjs
+++ /dev/null
@@ -1,66 +0,0 @@
-import fs from 'fs'
-import path from 'path'
-
-import babel from '@rollup/plugin-babel'
-import commonjs from '@rollup/plugin-commonjs'
-import resolve from '@rollup/plugin-node-resolve'
-import typescript from '@rollup/plugin-typescript'
-import peerDepsExternal from 'rollup-plugin-peer-deps-external'
-import { uglify } from 'rollup-plugin-uglify'
-
-import pkg from './package.json' assert { type: 'json' }
-
-const AVAILABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.json']
-
-function getInputFileFromExports (exportsPath) {
-    const isDirectory = fs.existsSync(exportsPath) && fs.lstatSync(exportsPath).isDirectory()
-    // NOTE: inputs are controlled by package.json exports field
-    // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
-    const pathToFile = isDirectory ? path.join(exportsPath, 'index') : exportsPath
-
-    const ext = AVAILABLE_EXTENSIONS.find(ext => fs.existsSync(`${pathToFile}${ext}`))
-    if (!ext) throw new Error('Unknown extension')
-
-    return `${pathToFile}${ext}`
-}
-
-/** @type {Array<import('rollup').RollupOptions>} */
-const options =  Object.entries(pkg.exports).map(([relativeImport, relativeMap]) => {
-    const input = getInputFileFromExports(path.join('src', relativeImport))
-    const output = []
-    if (relativeMap && relativeMap.require) {
-        output.push({
-            file: relativeMap.require,
-            format: 'cjs',
-            sourcemap: true,
-        })
-    }
-    if (relativeMap && relativeMap.import) {
-        output.push({
-            file: relativeMap.import,
-            format: 'esm',
-            sourcemap: true,
-        })
-    }
-
-    return {
-        input,
-        output,
-        plugins: [
-            peerDepsExternal(),
-            resolve(),
-            commonjs(),
-            babel({
-                babelHelpers: 'bundled',
-                exclude: 'node_modules/**',
-                presets: [['@babel/preset-react', { 'runtime': 'automatic' }]],
-                extensions: AVAILABLE_EXTENSIONS,
-            }),
-            typescript({ tsconfig: './tsconfig.json', compilerOptions: { declaration: false } }),
-            uglify(),
-        ],
-        external: ['react', 'react-dom'],
-    }
-})
-
-export default options
\ No newline at end of file
diff --git a/packages/miniapp-utils/src/helpers/analytics/index.ts b/packages/miniapp-utils/src/helpers/analytics/index.ts
new file mode 100644
index 00000000000..4436e5cb4b7
--- /dev/null
+++ b/packages/miniapp-utils/src/helpers/analytics/index.ts
@@ -0,0 +1,2 @@
+export { Analytics } from './instance'
+export type { AnalyticsPlugin, AnalyticsConfig } from './types'
\ No newline at end of file
diff --git a/packages/miniapp-utils/src/helpers/analytics/instance.ts b/packages/miniapp-utils/src/helpers/analytics/instance.ts
new file mode 100644
index 00000000000..452b11d5ca8
--- /dev/null
+++ b/packages/miniapp-utils/src/helpers/analytics/instance.ts
@@ -0,0 +1,182 @@
+import { Analytics as DefaultAnalytics } from 'analytics'
+
+import { GroupingMiddlewarePlugin } from './middlewares'
+
+import type { AnyPayload, AnalyticsConfig, AnalyticsInstanceWithGroups, PageData } from './types'
+
+/**
+ * Type-safe wrapper on top of "[analytics](https://www.npmjs.com/package/analytics)" npm package
+ * with support of group method (Posthog inspired) to group users into cohorts
+ *
+ * @example Init analytics in your app with type-guards
+ * import { Analytics } from '@open-condo/miniapp-utils/helpers/analytics'
+ * import { isDebug } from '@open-condo/miniapp-utils/helpers/environment'
+ *
+ * type MyAppEvents = {
+ *     'order_create': { orderId: string, itemIds: Array<string> }
+ *     'user_register': { userId: string, utmSource?: string  }
+ * }
+ *
+ * type MyUserData = {
+ *     'name'?: string
+ *     'age'?: number
+ * }
+ *
+ * type MyAppGroups = 'organization' | 'country'
+ *
+ * export const analytics = new Analytics<MyAppEvents, MyUserData, MyAppGroups>({
+ *     app: appName,
+ *     version: revision,
+ *     debug: isDebug(),
+ * })
+ *
+ * @example Use initialized analytics in your-app
+ * import { Router } from 'next/router'
+ * import { useEffect } from 'react'
+ *
+ * import { isValidCondoUIMessage } from '@open-condo/ui/events'
+ *
+ * import { analytics } from '@/domains/common/utils/analytics'
+ * import { useAuth } from '@/domains/user/utils/auth'
+ *
+ * import type { FC } from 'react'
+ *
+ * export const ResidentAppEventsHandler: FC = () => {
+ *     const user = useAuth()
+ *     const { activeResident } = useActiveResident()
+ *
+ *     // User change tracking
+ *     useEffect(() => {
+ *         if (user) {
+ *             analytics.identify(user.id, { name: user.name, type: user.type })
+ *         }
+ *     }, [user])
+ *
+ *     // Page views tracking
+ *     useEffect(() => {
+ *         const handleRouteChange = () => analytics.pageView()
+ *         Router.events.on('routeChangeComplete', handleRouteChange)
+ *
+ *         return () => {
+ *             Router.events.off('routeChangeComplete', handleRouteChange)
+ *         }
+ *     }, [])
+ *
+ *     // Condo UI events tracking
+ *     useEffect(() => {
+ *         if (typeof window !== 'undefined') {
+ *             const handleMessage = async (e: MessageEvent) => {
+ *                 if (isValidCondoUIMessage(e)) {
+ *                     const { params: { event, ...eventData } } = e.data
+ *                     await analytics.trackUntyped(event, eventData)
+ *                 }
+ *
+ *             }
+ *
+ *             window.addEventListener('message', handleMessage)
+ *
+ *             return () => {
+ *                 window.removeEventListener('message', handleMessage)
+ *             }
+ *         }
+ *     }, [])
+ *
+ *     return null
+ * }
+ */
+export class Analytics<
+    Events extends Record<string, AnyPayload> = Record<string, never>,
+    UserData extends AnyPayload = Record<string, never>,
+    GroupNames extends string = never,
+> {
+    private readonly _analytics: AnalyticsInstanceWithGroups<GroupNames>
+    private readonly _groups = new Set<GroupNames>()
+
+    constructor (config: AnalyticsConfig) {
+        this._analytics = DefaultAnalytics({
+            ...config,
+            plugins: [
+                GroupingMiddlewarePlugin,
+                ...(config.plugins || []),
+            ],
+        }) as AnalyticsInstanceWithGroups<GroupNames>
+        this._analytics.groups = this._groups
+    }
+
+    /**
+     * Tracks type-safe business events. Recommended to use in most cases in app's codebase.
+     * To add an event, modify "Events" generic.
+     */
+    async track<EventName extends Extract<keyof Events, string>>(eventName: EventName, eventData: Events[EventName]): Promise<void> {
+        await this._analytics.track(eventName, eventData)
+    }
+
+    /**
+     * Tracks untyped analytics events, used mainly for external sources (bridge / ui-kit / messages, etc.)
+     * @deprecated It's not recommended to use this in your business logic, consider using typed "track" instead
+     */
+    async trackUntyped (eventName: string, eventData: AnyPayload): Promise<void> {
+        await this._analytics.track(eventName, eventData)
+    }
+
+    /**
+     * Tracks page changing in SPAs
+     */
+    async pageView (data?: PageData): Promise<void> {
+        await this._analytics.page(data)
+    }
+
+    /**
+     * Identifies user in analytics provider.
+     * To specify all possible shape of user's data, modify "UserData" generic
+     *
+     * NOTE: Analytics plugins don't have a fixed behavior on how to handle consecutive identify calls.
+     * Some of them affect only subsequent events, others affect all user events.
+     * Therefore, it is not recommended to put cohort-specific data (organization, address, language, etc.) here.
+     * Instead, use something like "group" method if your plugins supports it.
+     */
+    async identify<Key extends keyof UserData> (userId: string, userData?: Pick<UserData, Key>): Promise<void> {
+        await this._analytics.identify(userId, userData)
+    }
+
+    /**
+     * Resets analytics providers
+     */
+    async reset (): Promise<void> {
+        for (const groupName of this._groups) {
+            const groupKey = Analytics.getGroupKey(groupName)
+            this._analytics.storage.removeItem(groupKey)
+        }
+        this._groups.clear()
+        await this._analytics.reset()
+    }
+
+    static getGroupKey (groupName: string): string {
+        return ['analytics', 'groups', groupName].join(':')
+    }
+
+    /**
+     * Associates the user with a group, adding the attributes `groups.${groupName} = groupId`
+     * to all subsequent analytic queries for the user
+     * @example
+     * analytics.setGroup('organization', organizationId)
+     */
+    setGroup (groupName: GroupNames, groupId: string): void {
+        const groupKey = Analytics.getGroupKey(groupName)
+        this._groups.add(groupName)
+        this._analytics.storage.setItem(groupKey, groupId)
+    }
+
+    /**
+     * Removes the current user from the group, stripping the “groups.${groupName}”
+     * attribute from all subsequent eventualities
+     * @example
+     * deleteOrganization()
+     *     .then(() => analytics.removeGroup('organization'))
+     */
+    removeGroup (groupName: GroupNames): void {
+        const groupKey = Analytics.getGroupKey(groupName)
+        this._analytics.storage.removeItem(groupKey)
+        this._groups.delete(groupName)
+    }
+}
diff --git a/packages/miniapp-utils/src/helpers/analytics/middlewares.ts b/packages/miniapp-utils/src/helpers/analytics/middlewares.ts
new file mode 100644
index 00000000000..7168e3b281a
--- /dev/null
+++ b/packages/miniapp-utils/src/helpers/analytics/middlewares.ts
@@ -0,0 +1,23 @@
+import { Analytics } from './instance'
+
+import type { AnalyticsPlugin, PluginTrackData } from './types'
+
+function _addGroupingProperties (data: PluginTrackData): PluginTrackData {
+    const { instance } = data
+    for (const groupName of instance.groups) {
+        const groupKey = Analytics.getGroupKey(groupName)
+        const groupValue = instance.storage.getItem(groupKey)
+
+        if (typeof groupValue === 'string') {
+            const groupAttrName = `groups.${groupName}`
+            data.payload.properties[groupAttrName] = groupValue
+        }
+    }
+    return data
+}
+
+export const GroupingMiddlewarePlugin: AnalyticsPlugin = {
+    name: 'analytics-plugin-grouping',
+    track: _addGroupingProperties,
+    page: _addGroupingProperties,
+}
\ No newline at end of file
diff --git a/packages/miniapp-utils/src/helpers/analytics/types.ts b/packages/miniapp-utils/src/helpers/analytics/types.ts
new file mode 100644
index 00000000000..ff0b7ca83e1
--- /dev/null
+++ b/packages/miniapp-utils/src/helpers/analytics/types.ts
@@ -0,0 +1,24 @@
+import type { AnalyticsInstance, AnalyticsPlugin } from 'analytics'
+
+export type AnalyticsConfig = {
+    app?: string
+    version?: string | number
+    debug?: boolean
+    plugins?: AnalyticsPlugin[]
+}
+
+export type AnalyticsInstanceWithGroups<GroupNames extends string> = AnalyticsInstance & {
+    groups: Set<GroupNames>
+}
+
+export type AnyPayload = Record<string, any>
+
+export type PluginTrackData = AnyPayload & {
+    abort(): void
+    instance: AnalyticsInstanceWithGroups<string>
+    payload: AnyPayload & {
+        properties: AnyPayload
+    }
+}
+
+export type { AnalyticsPlugin, PageData } from 'analytics'
\ No newline at end of file
diff --git a/packages/miniapp-utils/src/helpers/posthog.ts b/packages/miniapp-utils/src/helpers/posthog.ts
new file mode 100644
index 00000000000..487b6545882
--- /dev/null
+++ b/packages/miniapp-utils/src/helpers/posthog.ts
@@ -0,0 +1,48 @@
+type Rewrite = {
+    source: string
+    destination: string
+}
+
+const POSTHOG_CLOUD_HOST_BASE = 'i.posthog.com'
+const POSTHOG_CLOUD_HOST_MATCHER = new RegExp(`^(\\w+)\\.${POSTHOG_CLOUD_HOST_BASE.replaceAll('.', '\\.')}$`)
+
+/**
+ * Generates Next.js rewrites based on PostHog domain,
+ * so that PostHog can run in cloud and self-hosted versions without any ad blocker restrictions
+ * @example
+ * generateRewrites('https://eu.i.posthog.com', '/api/posthog')
+ * generateRewrites('https://posthog.my.domain.com', '/api/posthog')
+ * generateRewrites(process.env.NEXT_PUBLIC_POSTHOG_HOST, '/api/posthog')
+ */
+export function generateRewrites (postHogDomain: string, routeEndpoint: string): Array<Rewrite> {
+    const url = new URL(postHogDomain)
+
+    const match = url.host.match(POSTHOG_CLOUD_HOST_MATCHER)
+
+    // Cloud PH must have separate url for assets
+    // SRC: https://posthog.com/docs/advanced/proxy/nextjs
+    if (match && match.length > 1) {
+        const region = match[1]
+        return [
+            {
+                source: `${routeEndpoint}/static/:path*`,
+                destination: `https://${region}-assets.${POSTHOG_CLOUD_HOST_BASE}/static/:path*`,
+            },
+            {
+                source: `${routeEndpoint}/:path*`,
+                destination: `https://${region}.${POSTHOG_CLOUD_HOST_BASE}/:path*`,
+            },
+            {
+                source: `${routeEndpoint}/decide`,
+                destination: `https://${region}.${POSTHOG_CLOUD_HOST_BASE}/decide`,
+            },
+        ]
+    }
+
+    return [
+        {
+            source: `${routeEndpoint}/:path*`,
+            destination: `${postHogDomain}/:path*`,
+        },
+    ]
+}
\ No newline at end of file
diff --git a/packages/miniapp-utils/tsup.config.mjs b/packages/miniapp-utils/tsup.config.mjs
new file mode 100644
index 00000000000..083b24ec20c
--- /dev/null
+++ b/packages/miniapp-utils/tsup.config.mjs
@@ -0,0 +1,37 @@
+import fs from 'fs'
+import { createRequire } from 'node:module'
+import path from 'path'
+
+import { defineConfig } from 'tsup'
+
+const require = createRequire(import.meta.url)
+const pkg = require('./package.json')
+const AVAILABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.json']
+
+function getInputFileFromExports (exportsPath) {
+    const isDirectory = fs.existsSync(exportsPath) && fs.lstatSync(exportsPath).isDirectory()
+    // NOTE: inputs are controlled by package.json exports field
+    // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
+    const pathToFile = isDirectory ? path.join(exportsPath, 'index') : exportsPath
+
+    const ext = AVAILABLE_EXTENSIONS.find(ext => fs.existsSync(`${pathToFile}${ext}`))
+    if (!ext) throw new Error('Unknown extension')
+
+    // NOTE: tsup expect linux-styled path with "/"
+    // https://github.com/egoist/tsup/pull/1300
+    return `${pathToFile}${ext}`.split(path.win32.sep).join(path.posix.sep)
+}
+
+export default defineConfig({
+    // Controlled traversal
+    // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
+    entry: Object.keys(pkg.exports).map(entry => getInputFileFromExports(path.join('.', 'src', entry))),
+    clean: true,
+    dts: true,
+    sourcemap: true,
+    splitting: false,
+    format: ['cjs', 'esm'],
+    target: 'node16',
+    minify: false,
+    external: Object.keys(pkg.peerDependencies),
+})
\ No newline at end of file
diff --git a/packages/ui/package.json b/packages/ui/package.json
index cb11379d757..cc5f514f2a8 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -53,7 +53,8 @@
     "./dist/": "./dist/",
     "./style-vars/css": "./dist/style-vars/variables.css",
     "./style-vars/less": "./dist/style-vars/variables.less",
-    "./hooks": "./dist/hooks.js"
+    "./hooks": "./dist/hooks.js",
+    "./events": "./dist/events.js"
   },
   "typesVersions": {
     "*": {
@@ -62,6 +63,9 @@
       ],
       "colors": [
         "dist/colors/index.d.ts"
+      ],
+      "events": [
+        "dist/events.d.ts"
       ]
     }
   },
@@ -129,6 +133,7 @@
     "classnames": "^2.3.2",
     "react-markdown": "^6",
     "react-phone-input-2": "^2.15.1",
-    "remark-gfm": "^1"
+    "remark-gfm": "^1",
+    "zod": "^3.24.2"
   }
 }
diff --git a/packages/ui/src/components/_utils/analytics.ts b/packages/ui/src/components/_utils/analytics.ts
index 0a424ef48f8..f54a1147615 100644
--- a/packages/ui/src/components/_utils/analytics.ts
+++ b/packages/ui/src/components/_utils/analytics.ts
@@ -62,7 +62,7 @@ export type AnalyticsParams = AnalyticsClickEventParams<ComponentNames['click']>
 | AnalyticsCheckEventParams<ComponentNames['check']>
 | AnalyticsChangeEventParams<ComponentNames['change']>
 
-const ANALYTICS_HANDLER_NAME = 'CondoWebSendAnalyticsEvent'
+export const ANALYTICS_HANDLER_NAME = 'CondoWebSendAnalyticsEvent'
 
 export function extractChildrenContent (children: React.ReactNode | React.ReactInstance): string | null {
     if (isString(children)) {
diff --git a/packages/ui/src/events.ts b/packages/ui/src/events.ts
new file mode 100644
index 00000000000..f01e1d5a9fc
--- /dev/null
+++ b/packages/ui/src/events.ts
@@ -0,0 +1,60 @@
+import { z } from 'zod'
+
+import { ANALYTICS_HANDLER_NAME } from './components/_utils/analytics'
+
+import type { AnalyticsParams } from './components/_utils/analytics'
+
+// NOTE: catchall is used to validate basic properties values.
+// We don't want to validate "exact" shape of the event, since it can differ from version to version
+const analyticsEventSchema = z.object({
+    event: z.enum(['click', 'check', 'change']),
+    component: z.string(),
+    location: z.string(),
+}).catchall(z.union([z.string(), z.array(z.string()), z.number(), z.boolean(), z.undefined()]))
+
+const condoMessageDataSchema = z.strictObject({
+    handler: z.literal(ANALYTICS_HANDLER_NAME),
+    params: analyticsEventSchema,
+    type: z.literal('condo-ui'),
+    version: z.string(),
+})
+const MAX_ALLOWED_EVENT_PARAMS = 10
+
+type CondoUIMessageDataType = z.infer<typeof condoMessageDataSchema>
+
+/**
+ * Checks if analytics parameters are valid
+ */
+export function isValidAnalyticsParams (params: unknown): params is AnalyticsParams {
+    const { success, data } = analyticsEventSchema.safeParse(params)
+    if (!success) {
+        return false
+    }
+
+    // NOTE: prevent overloaded messages
+    const isCompactEnough = Object.keys(data).length < MAX_ALLOWED_EVENT_PARAMS
+    if (!isCompactEnough) {
+        console.warn(`Condo UI event "${data?.event}" is containing too many parameters`)
+    }
+
+    return isCompactEnough
+}
+
+/**
+ * Checks if incoming post-message is valid Condo UI message
+ */
+export function isValidCondoUIMessage (e: MessageEvent): e is MessageEvent<CondoUIMessageDataType> {
+    const { success, data } = condoMessageDataSchema.safeParse(e.data)
+
+    if (!success) {
+        return false
+    }
+
+    // NOTE: prevent overloaded messages
+    const isCompactEnough = Object.keys(data?.params).length < MAX_ALLOWED_EVENT_PARAMS
+    if (!isCompactEnough) {
+        console.warn(`Condo UI event "${data?.params.event}" is containing too many parameters`)
+    }
+
+    return isCompactEnough
+}
\ No newline at end of file
diff --git a/packages/ui/webpack.common.js b/packages/ui/webpack.common.js
index 830dd1d4a4a..f955518f557 100644
--- a/packages/ui/webpack.common.js
+++ b/packages/ui/webpack.common.js
@@ -17,6 +17,7 @@ module.exports = {
         'index': './src/index.ts',
         'colors/index': './src/colors/index.ts',
         'hooks': './src/hooks.ts',
+        'events': './src/events.ts',
     },
     output: {
         filename: '[name].js',
diff --git a/yarn.lock b/yarn.lock
index 2f23a2a75e4..3dcc84ad2b3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -130,6 +130,73 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@analytics/cookie-utils@npm:^0.2.12":
+  version: 0.2.12
+  resolution: "@analytics/cookie-utils@npm:0.2.12"
+  dependencies:
+    "@analytics/global-storage-utils": ^0.1.7
+  checksum: 2eafc326f8117b1204a39f9699ffb65c0ae8a6c37a826a51991105b320a7afc77d3f080a22af30ed3100189bcfe43a8f9dd55150ac28ff9493693cc167965b0c
+  languageName: node
+  linkType: hard
+
+"@analytics/core@npm:^0.12.17":
+  version: 0.12.17
+  resolution: "@analytics/core@npm:0.12.17"
+  dependencies:
+    "@analytics/global-storage-utils": ^0.1.7
+    "@analytics/type-utils": ^0.6.2
+    analytics-utils: ^1.0.14
+  checksum: bce30d340782ab21b7f1c4381c4798543c3b0a99fbb8f92da920ecedd201d80e5a484a4a4b5010da9329af4abd9d436e0006397eca9a09ee2af6f2ea0b3e36ab
+  languageName: node
+  linkType: hard
+
+"@analytics/global-storage-utils@npm:^0.1.7":
+  version: 0.1.7
+  resolution: "@analytics/global-storage-utils@npm:0.1.7"
+  dependencies:
+    "@analytics/type-utils": ^0.6.2
+  checksum: 82e52155121d46ee08fd684c2cf69e455eab7ab8ecb6c10f4777df48ea3fa1a4a08c19bf09d3d192efa1e4129fe64334410f0e8ecb637733b0c0af62cfed4c96
+  languageName: node
+  linkType: hard
+
+"@analytics/localstorage-utils@npm:^0.1.10":
+  version: 0.1.10
+  resolution: "@analytics/localstorage-utils@npm:0.1.10"
+  dependencies:
+    "@analytics/global-storage-utils": ^0.1.7
+  checksum: f147f643dc98452f6da0aeaa2d448735dd37a8114f4eae8719832e8daa459f5dc29a80435abe6e0313a5b6224e224ba45e1e7bbfa07259948dfeddb8c1af962a
+  languageName: node
+  linkType: hard
+
+"@analytics/session-storage-utils@npm:^0.0.7":
+  version: 0.0.7
+  resolution: "@analytics/session-storage-utils@npm:0.0.7"
+  dependencies:
+    "@analytics/global-storage-utils": ^0.1.7
+  checksum: c0389bf9c7eb13cc0ed2be125c1ffd80e8584e66f6525ed0ebbc29c7937904984032e856a0add04064722dfded1cf172c89f28125683157f3e828f6512586c40
+  languageName: node
+  linkType: hard
+
+"@analytics/storage-utils@npm:^0.4.2":
+  version: 0.4.2
+  resolution: "@analytics/storage-utils@npm:0.4.2"
+  dependencies:
+    "@analytics/cookie-utils": ^0.2.12
+    "@analytics/global-storage-utils": ^0.1.7
+    "@analytics/localstorage-utils": ^0.1.10
+    "@analytics/session-storage-utils": ^0.0.7
+    "@analytics/type-utils": ^0.6.2
+  checksum: 3e3497270eeabecbb784cf74ff7f0aa711ef4579563c34a37d3138b16eff4b1a5c2b7f95dcb27e18ae31c53a173267792548f0b687b57598aff164e1ac0f9cf6
+  languageName: node
+  linkType: hard
+
+"@analytics/type-utils@npm:^0.6.2":
+  version: 0.6.2
+  resolution: "@analytics/type-utils@npm:0.6.2"
+  checksum: 9aa10dae2ab2d444dff83c12d705672d1e7ae5d91f6c5ed98454c7ce0be9b0a76a3c420119752d33e276c718e89fca7eedecdbee025bde872ac29e2742093d17
+  languageName: node
+  linkType: hard
+
 "@ant-design/colors@npm:^6.0.0":
   version: 6.0.0
   resolution: "@ant-design/colors@npm:6.0.0"
@@ -2017,6 +2084,7 @@ __metadata:
     "@graphql-codegen/typescript": ^4.0.1
     "@graphql-codegen/typescript-operations": ^4.0.1
     "@graphql-codegen/typescript-react-apollo": ^4.0.0
+    "@metro-fs/analytics-plugin-posthog": ^1.14.0
     "@open-condo/apollo": "workspace:^"
     "@open-condo/config": "workspace:^"
     "@open-condo/icons": "workspace:^"
@@ -2027,8 +2095,10 @@ __metadata:
     "@parcel/watcher": ^2.3.0
     "@serwist/next": ^9.0.1
     "@types/big.js": ^6.2.2
+    "@types/node": ^16
     "@types/react": 18.0.27
     "@types/react-dom": 18.0.10
+    analytics: ^0.8.16
     antd: 5.16.4
     big.js: ^6.2.1
     classnames: ^2.3.2
@@ -8768,6 +8838,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/aix-ppc64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/aix-ppc64@npm:0.25.0"
+  conditions: os=aix & cpu=ppc64
+  languageName: node
+  linkType: hard
+
 "@esbuild/android-arm64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/android-arm64@npm:0.18.20"
@@ -8782,6 +8859,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/android-arm64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/android-arm64@npm:0.25.0"
+  conditions: os=android & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/android-arm@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/android-arm@npm:0.18.20"
@@ -8796,6 +8880,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/android-arm@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/android-arm@npm:0.25.0"
+  conditions: os=android & cpu=arm
+  languageName: node
+  linkType: hard
+
 "@esbuild/android-x64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/android-x64@npm:0.18.20"
@@ -8810,6 +8901,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/android-x64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/android-x64@npm:0.25.0"
+  conditions: os=android & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/darwin-arm64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/darwin-arm64@npm:0.18.20"
@@ -8824,6 +8922,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/darwin-arm64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/darwin-arm64@npm:0.25.0"
+  conditions: os=darwin & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/darwin-x64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/darwin-x64@npm:0.18.20"
@@ -8838,6 +8943,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/darwin-x64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/darwin-x64@npm:0.25.0"
+  conditions: os=darwin & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/freebsd-arm64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/freebsd-arm64@npm:0.18.20"
@@ -8852,6 +8964,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/freebsd-arm64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/freebsd-arm64@npm:0.25.0"
+  conditions: os=freebsd & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/freebsd-x64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/freebsd-x64@npm:0.18.20"
@@ -8866,6 +8985,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/freebsd-x64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/freebsd-x64@npm:0.25.0"
+  conditions: os=freebsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-arm64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/linux-arm64@npm:0.18.20"
@@ -8880,6 +9006,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-arm64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/linux-arm64@npm:0.25.0"
+  conditions: os=linux & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-arm@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/linux-arm@npm:0.18.20"
@@ -8894,6 +9027,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-arm@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/linux-arm@npm:0.25.0"
+  conditions: os=linux & cpu=arm
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-ia32@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/linux-ia32@npm:0.18.20"
@@ -8908,6 +9048,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-ia32@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/linux-ia32@npm:0.25.0"
+  conditions: os=linux & cpu=ia32
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-loong64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/linux-loong64@npm:0.18.20"
@@ -8922,6 +9069,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-loong64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/linux-loong64@npm:0.25.0"
+  conditions: os=linux & cpu=loong64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-mips64el@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/linux-mips64el@npm:0.18.20"
@@ -8936,6 +9090,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-mips64el@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/linux-mips64el@npm:0.25.0"
+  conditions: os=linux & cpu=mips64el
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-ppc64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/linux-ppc64@npm:0.18.20"
@@ -8950,6 +9111,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-ppc64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/linux-ppc64@npm:0.25.0"
+  conditions: os=linux & cpu=ppc64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-riscv64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/linux-riscv64@npm:0.18.20"
@@ -8964,6 +9132,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-riscv64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/linux-riscv64@npm:0.25.0"
+  conditions: os=linux & cpu=riscv64
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-s390x@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/linux-s390x@npm:0.18.20"
@@ -8978,6 +9153,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-s390x@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/linux-s390x@npm:0.25.0"
+  conditions: os=linux & cpu=s390x
+  languageName: node
+  linkType: hard
+
 "@esbuild/linux-x64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/linux-x64@npm:0.18.20"
@@ -8992,6 +9174,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/linux-x64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/linux-x64@npm:0.25.0"
+  conditions: os=linux & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/netbsd-arm64@npm:0.24.2":
   version: 0.24.2
   resolution: "@esbuild/netbsd-arm64@npm:0.24.2"
@@ -8999,6 +9188,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/netbsd-arm64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/netbsd-arm64@npm:0.25.0"
+  conditions: os=netbsd & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/netbsd-x64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/netbsd-x64@npm:0.18.20"
@@ -9013,6 +9209,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/netbsd-x64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/netbsd-x64@npm:0.25.0"
+  conditions: os=netbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/openbsd-arm64@npm:0.24.2":
   version: 0.24.2
   resolution: "@esbuild/openbsd-arm64@npm:0.24.2"
@@ -9020,6 +9223,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/openbsd-arm64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/openbsd-arm64@npm:0.25.0"
+  conditions: os=openbsd & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/openbsd-x64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/openbsd-x64@npm:0.18.20"
@@ -9034,6 +9244,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/openbsd-x64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/openbsd-x64@npm:0.25.0"
+  conditions: os=openbsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/sunos-x64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/sunos-x64@npm:0.18.20"
@@ -9048,6 +9265,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/sunos-x64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/sunos-x64@npm:0.25.0"
+  conditions: os=sunos & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@esbuild/win32-arm64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/win32-arm64@npm:0.18.20"
@@ -9062,6 +9286,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/win32-arm64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/win32-arm64@npm:0.25.0"
+  conditions: os=win32 & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@esbuild/win32-ia32@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/win32-ia32@npm:0.18.20"
@@ -9076,6 +9307,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/win32-ia32@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/win32-ia32@npm:0.25.0"
+  conditions: os=win32 & cpu=ia32
+  languageName: node
+  linkType: hard
+
 "@esbuild/win32-x64@npm:0.18.20":
   version: 0.18.20
   resolution: "@esbuild/win32-x64@npm:0.18.20"
@@ -9090,6 +9328,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@esbuild/win32-x64@npm:0.25.0":
+  version: 0.25.0
+  resolution: "@esbuild/win32-x64@npm:0.25.0"
+  conditions: os=win32 & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@eslint/eslintrc@npm:^0.4.3":
   version: 0.4.3
   resolution: "@eslint/eslintrc@npm:0.4.3"
@@ -13552,6 +13797,15 @@ __metadata:
   languageName: node
   linkType: soft
 
+"@metro-fs/analytics-plugin-posthog@npm:^1.14.0":
+  version: 1.14.0
+  resolution: "@metro-fs/analytics-plugin-posthog@npm:1.14.0"
+  dependencies:
+    posthog-js: ^1.116.6
+  checksum: fdedb88f48e90ebb994e4a102ce37b5b5831d095a3326917f36a5caccdc39b3158097a41d153bb57c39f83e1f8532a8ffb4dfe5d39405c4918412b644142cfa0
+  languageName: node
+  linkType: hard
+
 "@microsoft/fetch-event-source@npm:2.0.1":
   version: 2.0.1
   resolution: "@microsoft/fetch-event-source@npm:2.0.1"
@@ -14364,27 +14618,19 @@ __metadata:
   resolution: "@open-condo/miniapp-utils@workspace:packages/miniapp-utils"
   dependencies:
     "@apollo/client": ^3.11.8
-    "@babel/preset-react": ^7.24.7
     "@open-condo/tsconfig": "workspace:^"
-    "@rollup/plugin-babel": ^6.0.4
-    "@rollup/plugin-commonjs": ^26.0.1
-    "@rollup/plugin-node-resolve": ^15.2.3
-    "@rollup/plugin-typescript": ^11.1.6
     "@types/react": ^16.14.61
-    babel-core: ^6.26.3
-    babel-loader: ^9.1.3
+    analytics: ^0.8.16
     cookie: ^0.6.0
     cookies-next: ^4.2.1
     graphql: ^16.8.1
     react: ^16.14.0
     react-dom: ^16.14.0
-    rimraf: ^6.0.1
-    rollup: ^4.28.0
-    rollup-plugin-peer-deps-external: ^2.2.4
-    rollup-plugin-uglify: ^6.0.4
+    tsup: ^8.4.0
     typescript: ^5.5.4
   peerDependencies:
     "@apollo/client": ">=3.0.0"
+    analytics: ^0.8.16
     react: ">=16.0.0"
     react-dom: ">=16.0.0"
   languageName: unknown
@@ -14479,6 +14725,7 @@ __metadata:
     webpack: ^5.91.0
     webpack-cli: ^4.10.0
     webpack-merge: ^5.8.0
+    zod: ^3.24.2
   peerDependencies:
     react: ">=16"
     react-dom: ">=16"
@@ -16476,6 +16723,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-android-arm-eabi@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-android-arm-eabi@npm:4.34.9"
+  conditions: os=android & cpu=arm
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-android-arm64@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-android-arm64@npm:4.14.3"
@@ -16497,6 +16751,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-android-arm64@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-android-arm64@npm:4.34.9"
+  conditions: os=android & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-darwin-arm64@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-darwin-arm64@npm:4.14.3"
@@ -16518,6 +16779,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-darwin-arm64@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-darwin-arm64@npm:4.34.9"
+  conditions: os=darwin & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-darwin-x64@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-darwin-x64@npm:4.14.3"
@@ -16539,6 +16807,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-darwin-x64@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-darwin-x64@npm:4.34.9"
+  conditions: os=darwin & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-freebsd-arm64@npm:4.28.0":
   version: 4.28.0
   resolution: "@rollup/rollup-freebsd-arm64@npm:4.28.0"
@@ -16553,6 +16828,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-freebsd-arm64@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-freebsd-arm64@npm:4.34.9"
+  conditions: os=freebsd & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-freebsd-x64@npm:4.28.0":
   version: 4.28.0
   resolution: "@rollup/rollup-freebsd-x64@npm:4.28.0"
@@ -16567,6 +16849,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-freebsd-x64@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-freebsd-x64@npm:4.34.9"
+  conditions: os=freebsd & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-arm-gnueabihf@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.14.3"
@@ -16588,6 +16877,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-arm-gnueabihf@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.34.9"
+  conditions: os=linux & cpu=arm & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-arm-musleabihf@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.14.3"
@@ -16609,6 +16905,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-arm-musleabihf@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.34.9"
+  conditions: os=linux & cpu=arm & libc=musl
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-arm64-gnu@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.14.3"
@@ -16630,6 +16933,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-arm64-gnu@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.34.9"
+  conditions: os=linux & cpu=arm64 & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-arm64-musl@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-linux-arm64-musl@npm:4.14.3"
@@ -16651,6 +16961,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-arm64-musl@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-arm64-musl@npm:4.34.9"
+  conditions: os=linux & cpu=arm64 & libc=musl
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-loongarch64-gnu@npm:4.34.6":
   version: 4.34.6
   resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.34.6"
@@ -16658,6 +16975,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-loongarch64-gnu@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.34.9"
+  conditions: os=linux & cpu=loong64 & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-powerpc64le-gnu@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.14.3"
@@ -16679,6 +17003,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-powerpc64le-gnu@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.34.9"
+  conditions: os=linux & cpu=ppc64 & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-riscv64-gnu@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.14.3"
@@ -16700,6 +17031,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-riscv64-gnu@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.34.9"
+  conditions: os=linux & cpu=riscv64 & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-s390x-gnu@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.14.3"
@@ -16721,6 +17059,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-s390x-gnu@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.34.9"
+  conditions: os=linux & cpu=s390x & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-x64-gnu@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-linux-x64-gnu@npm:4.14.3"
@@ -16742,6 +17087,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-x64-gnu@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-x64-gnu@npm:4.34.9"
+  conditions: os=linux & cpu=x64 & libc=glibc
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-linux-x64-musl@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-linux-x64-musl@npm:4.14.3"
@@ -16763,6 +17115,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-linux-x64-musl@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-linux-x64-musl@npm:4.34.9"
+  conditions: os=linux & cpu=x64 & libc=musl
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-win32-arm64-msvc@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.14.3"
@@ -16784,6 +17143,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-win32-arm64-msvc@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.34.9"
+  conditions: os=win32 & cpu=arm64
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-win32-ia32-msvc@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.14.3"
@@ -16805,6 +17171,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-win32-ia32-msvc@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.34.9"
+  conditions: os=win32 & cpu=ia32
+  languageName: node
+  linkType: hard
+
 "@rollup/rollup-win32-x64-msvc@npm:4.14.3":
   version: 4.14.3
   resolution: "@rollup/rollup-win32-x64-msvc@npm:4.14.3"
@@ -16826,6 +17199,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/rollup-win32-x64-msvc@npm:4.34.9":
+  version: 4.34.9
+  resolution: "@rollup/rollup-win32-x64-msvc@npm:4.34.9"
+  conditions: os=win32 & cpu=x64
+  languageName: node
+  linkType: hard
+
 "@samverschueren/stream-to-observable@npm:^0.3.0":
   version: 0.3.1
   resolution: "@samverschueren/stream-to-observable@npm:0.3.1"
@@ -21358,6 +21738,28 @@ __metadata:
   languageName: node
   linkType: hard
 
+"analytics-utils@npm:^1.0.14":
+  version: 1.0.14
+  resolution: "analytics-utils@npm:1.0.14"
+  dependencies:
+    "@analytics/type-utils": ^0.6.2
+    dlv: ^1.1.3
+  peerDependencies:
+    "@types/dlv": ^1.0.0
+  checksum: a90baf9f40478d4afde1f11ac5e1f482bc8a89c415efff566499d490f277e075af49d36b7a799c2d441cca9fabf448b39eab718cfe75c290e5eddeca63c28bc4
+  languageName: node
+  linkType: hard
+
+"analytics@npm:^0.8.16":
+  version: 0.8.16
+  resolution: "analytics@npm:0.8.16"
+  dependencies:
+    "@analytics/core": ^0.12.17
+    "@analytics/storage-utils": ^0.4.2
+  checksum: 6c9ec14e845f55e630884b208464bd76571f3ced0cf5e21d9562916696a2b88922b82b63fc6b9f928cfc41bc1123b47f28b62cabd4a8b8b9ee2f9554a1f5292b
+  languageName: node
+  linkType: hard
+
 "anser@npm:1.4.9":
   version: 1.4.9
   resolution: "anser@npm:1.4.9"
@@ -24489,7 +24891,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"bundle-require@npm:^5.0.0":
+"bundle-require@npm:^5.0.0, bundle-require@npm:^5.1.0":
   version: 5.1.0
   resolution: "bundle-require@npm:5.1.0"
   dependencies:
@@ -25342,7 +25744,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"chokidar@npm:^4.0.1":
+"chokidar@npm:^4.0.1, chokidar@npm:^4.0.3":
   version: 4.0.3
   resolution: "chokidar@npm:4.0.3"
   dependencies:
@@ -26234,6 +26636,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"consola@npm:^3.4.0":
+  version: 3.4.0
+  resolution: "consola@npm:3.4.0"
+  checksum: 03d9ee487a53b710f53aeff18447a242d95c080aff051389b5ee49915bebb38cb31687e144e1bb3dd6ebcfc454fef566cc5912f6150c7cfe9349947ba09a5a87
+  languageName: node
+  linkType: hard
+
 "console-browserify@npm:^1.1.0":
   version: 1.2.0
   resolution: "console-browserify@npm:1.2.0"
@@ -26574,6 +26983,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"core-js@npm:^3.38.1":
+  version: 3.41.0
+  resolution: "core-js@npm:3.41.0"
+  checksum: 05331e92f354d3b92fa296fb3fc90c7b25d1a65230046c68651be29d67245bb156bdde7bf5c8cf8b3ecddfff93273a9ba6567f5e4ee6ceb70f39cee430693509
+  languageName: node
+  linkType: hard
+
 "core-js@npm:^3.6.0, core-js@npm:^3.8.3":
   version: 3.24.1
   resolution: "core-js@npm:3.24.1"
@@ -28444,6 +28860,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"dlv@npm:^1.1.3":
+  version: 1.1.3
+  resolution: "dlv@npm:1.1.3"
+  checksum: d7381bca22ed11933a1ccf376db7a94bee2c57aa61e490f680124fa2d1cd27e94eba641d9f45be57caab4f9a6579de0983466f620a2cd6230d7ec93312105ae7
+  languageName: node
+  linkType: hard
+
 "docker-compose@npm:^0.24.8":
   version: 0.24.8
   resolution: "docker-compose@npm:0.24.8"
@@ -29832,6 +30255,92 @@ __metadata:
   languageName: node
   linkType: hard
 
+"esbuild@npm:^0.25.0":
+  version: 0.25.0
+  resolution: "esbuild@npm:0.25.0"
+  dependencies:
+    "@esbuild/aix-ppc64": 0.25.0
+    "@esbuild/android-arm": 0.25.0
+    "@esbuild/android-arm64": 0.25.0
+    "@esbuild/android-x64": 0.25.0
+    "@esbuild/darwin-arm64": 0.25.0
+    "@esbuild/darwin-x64": 0.25.0
+    "@esbuild/freebsd-arm64": 0.25.0
+    "@esbuild/freebsd-x64": 0.25.0
+    "@esbuild/linux-arm": 0.25.0
+    "@esbuild/linux-arm64": 0.25.0
+    "@esbuild/linux-ia32": 0.25.0
+    "@esbuild/linux-loong64": 0.25.0
+    "@esbuild/linux-mips64el": 0.25.0
+    "@esbuild/linux-ppc64": 0.25.0
+    "@esbuild/linux-riscv64": 0.25.0
+    "@esbuild/linux-s390x": 0.25.0
+    "@esbuild/linux-x64": 0.25.0
+    "@esbuild/netbsd-arm64": 0.25.0
+    "@esbuild/netbsd-x64": 0.25.0
+    "@esbuild/openbsd-arm64": 0.25.0
+    "@esbuild/openbsd-x64": 0.25.0
+    "@esbuild/sunos-x64": 0.25.0
+    "@esbuild/win32-arm64": 0.25.0
+    "@esbuild/win32-ia32": 0.25.0
+    "@esbuild/win32-x64": 0.25.0
+  dependenciesMeta:
+    "@esbuild/aix-ppc64":
+      optional: true
+    "@esbuild/android-arm":
+      optional: true
+    "@esbuild/android-arm64":
+      optional: true
+    "@esbuild/android-x64":
+      optional: true
+    "@esbuild/darwin-arm64":
+      optional: true
+    "@esbuild/darwin-x64":
+      optional: true
+    "@esbuild/freebsd-arm64":
+      optional: true
+    "@esbuild/freebsd-x64":
+      optional: true
+    "@esbuild/linux-arm":
+      optional: true
+    "@esbuild/linux-arm64":
+      optional: true
+    "@esbuild/linux-ia32":
+      optional: true
+    "@esbuild/linux-loong64":
+      optional: true
+    "@esbuild/linux-mips64el":
+      optional: true
+    "@esbuild/linux-ppc64":
+      optional: true
+    "@esbuild/linux-riscv64":
+      optional: true
+    "@esbuild/linux-s390x":
+      optional: true
+    "@esbuild/linux-x64":
+      optional: true
+    "@esbuild/netbsd-arm64":
+      optional: true
+    "@esbuild/netbsd-x64":
+      optional: true
+    "@esbuild/openbsd-arm64":
+      optional: true
+    "@esbuild/openbsd-x64":
+      optional: true
+    "@esbuild/sunos-x64":
+      optional: true
+    "@esbuild/win32-arm64":
+      optional: true
+    "@esbuild/win32-ia32":
+      optional: true
+    "@esbuild/win32-x64":
+      optional: true
+  bin:
+    esbuild: bin/esbuild
+  checksum: 4d1e0cb7c059a373ea3edb20ca5efcea29efada03e4ea82b2b8ab1f2f062e4791e9744213308775d26e07a0225a7d8250da93da5c8e07ef61bb93d58caab8cf9
+  languageName: node
+  linkType: hard
+
 "escalade@npm:^3.0.1, escalade@npm:^3.1.1":
   version: 3.1.1
   resolution: "escalade@npm:3.1.1"
@@ -31172,7 +31681,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"fdir@npm:^6.4.2":
+"fdir@npm:^6.4.2, fdir@npm:^6.4.3":
   version: 6.4.3
   resolution: "fdir@npm:6.4.3"
   peerDependencies:
@@ -45392,6 +45901,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"posthog-js@npm:^1.116.6":
+  version: 1.225.0
+  resolution: "posthog-js@npm:1.225.0"
+  dependencies:
+    core-js: ^3.38.1
+    fflate: ^0.4.8
+    preact: ^10.19.3
+    web-vitals: ^4.2.0
+  peerDependencies:
+    "@rrweb/types": 2.0.0-alpha.17
+  checksum: c54938e5e6a2b62cac7cb744f96c506fae966e49fe22d9163518544f1c6f4ece81f651b4cc30b3cab589bf67865047f3ebd11102d0e394e8a7ecfc2aa6831d14
+  languageName: node
+  linkType: hard
+
 "pprof-format@npm:^2.1.0":
   version: 2.1.0
   resolution: "pprof-format@npm:2.1.0"
@@ -45399,6 +45922,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"preact@npm:^10.19.3":
+  version: 10.26.4
+  resolution: "preact@npm:10.26.4"
+  checksum: 38e92630a86fbaa43a2b085f12275abebecba72165bab56b70f209116dae694ade056aadb2df3aead004e50e5f94848d730a0accf052e2c0f27afc09bafd256c
+  languageName: node
+  linkType: hard
+
 "prelude-ls@npm:^1.2.1":
   version: 1.2.1
   resolution: "prelude-ls@npm:1.2.1"
@@ -50264,6 +50794,78 @@ __metadata:
   languageName: node
   linkType: hard
 
+"rollup@npm:^4.34.8":
+  version: 4.34.9
+  resolution: "rollup@npm:4.34.9"
+  dependencies:
+    "@rollup/rollup-android-arm-eabi": 4.34.9
+    "@rollup/rollup-android-arm64": 4.34.9
+    "@rollup/rollup-darwin-arm64": 4.34.9
+    "@rollup/rollup-darwin-x64": 4.34.9
+    "@rollup/rollup-freebsd-arm64": 4.34.9
+    "@rollup/rollup-freebsd-x64": 4.34.9
+    "@rollup/rollup-linux-arm-gnueabihf": 4.34.9
+    "@rollup/rollup-linux-arm-musleabihf": 4.34.9
+    "@rollup/rollup-linux-arm64-gnu": 4.34.9
+    "@rollup/rollup-linux-arm64-musl": 4.34.9
+    "@rollup/rollup-linux-loongarch64-gnu": 4.34.9
+    "@rollup/rollup-linux-powerpc64le-gnu": 4.34.9
+    "@rollup/rollup-linux-riscv64-gnu": 4.34.9
+    "@rollup/rollup-linux-s390x-gnu": 4.34.9
+    "@rollup/rollup-linux-x64-gnu": 4.34.9
+    "@rollup/rollup-linux-x64-musl": 4.34.9
+    "@rollup/rollup-win32-arm64-msvc": 4.34.9
+    "@rollup/rollup-win32-ia32-msvc": 4.34.9
+    "@rollup/rollup-win32-x64-msvc": 4.34.9
+    "@types/estree": 1.0.6
+    fsevents: ~2.3.2
+  dependenciesMeta:
+    "@rollup/rollup-android-arm-eabi":
+      optional: true
+    "@rollup/rollup-android-arm64":
+      optional: true
+    "@rollup/rollup-darwin-arm64":
+      optional: true
+    "@rollup/rollup-darwin-x64":
+      optional: true
+    "@rollup/rollup-freebsd-arm64":
+      optional: true
+    "@rollup/rollup-freebsd-x64":
+      optional: true
+    "@rollup/rollup-linux-arm-gnueabihf":
+      optional: true
+    "@rollup/rollup-linux-arm-musleabihf":
+      optional: true
+    "@rollup/rollup-linux-arm64-gnu":
+      optional: true
+    "@rollup/rollup-linux-arm64-musl":
+      optional: true
+    "@rollup/rollup-linux-loongarch64-gnu":
+      optional: true
+    "@rollup/rollup-linux-powerpc64le-gnu":
+      optional: true
+    "@rollup/rollup-linux-riscv64-gnu":
+      optional: true
+    "@rollup/rollup-linux-s390x-gnu":
+      optional: true
+    "@rollup/rollup-linux-x64-gnu":
+      optional: true
+    "@rollup/rollup-linux-x64-musl":
+      optional: true
+    "@rollup/rollup-win32-arm64-msvc":
+      optional: true
+    "@rollup/rollup-win32-ia32-msvc":
+      optional: true
+    "@rollup/rollup-win32-x64-msvc":
+      optional: true
+    fsevents:
+      optional: true
+  bin:
+    rollup: dist/bin/rollup
+  checksum: ed7a5e382de5fe872abffdab614b7f745cbed7328cf4ced560c4d09214b3d30e167f8c7df9e8b63489497bdf3a6be07a2474f9ff3195026bdf2d49cdbeac38ae
+  languageName: node
+  linkType: hard
+
 "root@workspace:.":
   version: 0.0.0-use.local
   resolution: "root@workspace:."
@@ -53843,13 +54445,23 @@ __metadata:
   languageName: node
   linkType: hard
 
-"tinyexec@npm:^0.3.1":
+"tinyexec@npm:^0.3.1, tinyexec@npm:^0.3.2":
   version: 0.3.2
   resolution: "tinyexec@npm:0.3.2"
   checksum: bd491923020610bdeadb0d8cf5d70e7cbad5a3201620fd01048c9bf3b31ffaa75c33254e1540e13b993ce4e8187852b0b5a93057bb598e7a57afa2ca2048a35c
   languageName: node
   linkType: hard
 
+"tinyglobby@npm:^0.2.11":
+  version: 0.2.12
+  resolution: "tinyglobby@npm:0.2.12"
+  dependencies:
+    fdir: ^6.4.3
+    picomatch: ^4.0.2
+  checksum: ef9357fa1b2b661afdccd315cb4995f5f36bce948faaace68aae85fe57bdd8f837883045c88efc50d3186bac6586e4ae2f31026b9a3aac061b884217e6092e23
+  languageName: node
+  linkType: hard
+
 "tinyglobby@npm:^0.2.9":
   version: 0.2.10
   resolution: "tinyglobby@npm:0.2.10"
@@ -54565,6 +55177,47 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tsup@npm:^8.4.0":
+  version: 8.4.0
+  resolution: "tsup@npm:8.4.0"
+  dependencies:
+    bundle-require: ^5.1.0
+    cac: ^6.7.14
+    chokidar: ^4.0.3
+    consola: ^3.4.0
+    debug: ^4.4.0
+    esbuild: ^0.25.0
+    joycon: ^3.1.1
+    picocolors: ^1.1.1
+    postcss-load-config: ^6.0.1
+    resolve-from: ^5.0.0
+    rollup: ^4.34.8
+    source-map: 0.8.0-beta.0
+    sucrase: ^3.35.0
+    tinyexec: ^0.3.2
+    tinyglobby: ^0.2.11
+    tree-kill: ^1.2.2
+  peerDependencies:
+    "@microsoft/api-extractor": ^7.36.0
+    "@swc/core": ^1
+    postcss: ^8.4.12
+    typescript: ">=4.5.0"
+  peerDependenciesMeta:
+    "@microsoft/api-extractor":
+      optional: true
+    "@swc/core":
+      optional: true
+    postcss:
+      optional: true
+    typescript:
+      optional: true
+  bin:
+    tsup: dist/cli-default.js
+    tsup-node: dist/cli-node.js
+  checksum: 8c632adcec8b91cb266e3179f92390d994da2f744fcea066932ca3f4332db06aae5363655a035fd3d4a215985f362763b22f1051744552b7fb6f90dc1a95043c
+  languageName: node
+  linkType: hard
+
 "tsutils@npm:^3.21.0":
   version: 3.21.0
   resolution: "tsutils@npm:3.21.0"
@@ -56430,6 +57083,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"web-vitals@npm:^4.2.0":
+  version: 4.2.4
+  resolution: "web-vitals@npm:4.2.4"
+  checksum: 5b3ffe1db33f23aebf8cc8560ac574401a95939baafde5841835c1bb1c01f9a2478442f319f77aa0d7914739fc2f6b020c5d5b128c16c5c77ca6be2f9dfbbde6
+  languageName: node
+  linkType: hard
+
 "webcrypto-core@npm:^1.7.4":
   version: 1.7.5
   resolution: "webcrypto-core@npm:1.7.5"


Reviews:
user: login: toplenboren
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T06:00:36Z
user: login: Alllex202
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:44:10Z
user: login: nomerdvadcatpyat
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-05T10:40:52Z
user: login: SavelevMatthew
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:39:43Z
user: login: SavelevMatthew
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T12:40:21Z

Review comments:
diff_hunk: @@ -0,0 +1,24 @@
+import type { AnalyticsInstance, AnalyticsPlugin } from 'analytics'
+
+export type AnalyticsConfig = {
+    app?: string
+    version?: string | number
path: packages/miniapp-utils/src/helpers/analytics/types.ts
user: login: Alllex202
body: what is version used for? is it the version of the app?
created_at: 2025-03-05T09:34:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,42 @@
+import { z } from 'zod'
+
+import { ANALYTICS_HANDLER_NAME } from './components/_utils/analytics'
+
+import type { AnalyticsParams } from './components/_utils/analytics'
+
+// NOTE: catchall is used to validate basic properties values.
+// We don't want to validate "exact" shape of the event, since it can differ from version to version
+const analyticsEventSchema = z.object({
+    event: z.enum(['click', 'check', 'change']),
+    component: z.string(),
+    location: z.string(),
+}).catchall(z.union([z.string(), z.array(z.string()), z.number(), z.boolean(), z.undefined()]))
+
+const condoMessageDataSchema = z.strictObject({
+    handler: z.literal(ANALYTICS_HANDLER_NAME),
+    params: analyticsEventSchema,
+    type: z.literal('condo-ui'),
+    version: z.string(),
+})
+
+type CondoUIMessageDataType = z.infer<typeof condoMessageDataSchema>
+
+/**
+ * Checks if analytics parameters are valid
+ */
+export function isValidAnalyticsParams (params: unknown): params is AnalyticsParams {
+    const { success, data } = analyticsEventSchema.safeParse(params)
+    if (!success) {
+        return false
+    }
+
+    // NOTE: prevent overloaded messages
+    return Object.keys(data).length < 10
path: packages/ui/src/events.ts
user: login: Alllex202
body: let's write something to the console if the parameters are invalid or there are too many messages
created_at: 2025-03-05T09:43:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,24 @@
+import type { AnalyticsInstance, AnalyticsPlugin } from 'analytics'
+
+export type AnalyticsConfig = {
+    app?: string
+    version?: string | number
path: packages/miniapp-utils/src/helpers/analytics/types.ts
user: login: SavelevMatthew
body: Yep, that's apps version:
https://getanalytics.io/api/#configuration
created_at: 2025-03-05T12:39:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,42 @@
+import { z } from 'zod'
+
+import { ANALYTICS_HANDLER_NAME } from './components/_utils/analytics'
+
+import type { AnalyticsParams } from './components/_utils/analytics'
+
+// NOTE: catchall is used to validate basic properties values.
+// We don't want to validate "exact" shape of the event, since it can differ from version to version
+const analyticsEventSchema = z.object({
+    event: z.enum(['click', 'check', 'change']),
+    component: z.string(),
+    location: z.string(),
+}).catchall(z.union([z.string(), z.array(z.string()), z.number(), z.boolean(), z.undefined()]))
+
+const condoMessageDataSchema = z.strictObject({
+    handler: z.literal(ANALYTICS_HANDLER_NAME),
+    params: analyticsEventSchema,
+    type: z.literal('condo-ui'),
+    version: z.string(),
+})
+
+type CondoUIMessageDataType = z.infer<typeof condoMessageDataSchema>
+
+/**
+ * Checks if analytics parameters are valid
+ */
+export function isValidAnalyticsParams (params: unknown): params is AnalyticsParams {
+    const { success, data } = analyticsEventSchema.safeParse(params)
+    if (!success) {
+        return false
+    }
+
+    // NOTE: prevent overloaded messages
+    return Object.keys(data).length < 10
path: packages/ui/src/events.ts
user: login: SavelevMatthew
body: Oke
created_at: 2025-03-05T12:40:21Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-06T07:16:02Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=open-condo-software_condo&pullRequest=5868) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=open-condo-software_condo&pullRequest=5868&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=open-condo-software_condo&pullRequest=5868&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=open-condo-software_condo&pullRequest=5868&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=open-condo-software_condo&pullRequest=5868&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=open-condo-software_condo&pullRequest=5868&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=open-condo-software_condo&pullRequest=5868)


author_association: NONE