From ae7fef8b356bb84c494c128af8b69ab256be4336 Mon Sep 17 00:00:00 2001 From: vmikhalevskii Date: Wed, 5 Aug 2026 11:47:25 +0300 Subject: [PATCH 1/5] chore: add @amplitude/analytics-react-native --- apps/mobile/package.json | 1 + package-lock.json | 118 +++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 120 insertions(+) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index ea885ed..a9e2988 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -24,6 +24,7 @@ "ios": "expo run:ios" }, "dependencies": { + "@amplitude/analytics-react-native": "^1.6.8", "@expo/metro-config": "~57.0.0", "@expo/metro-runtime": "~57.0.7", "@expo/ui": "~57.0.7", diff --git a/package-lock.json b/package-lock.json index e7045e5..263cc25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "apps/*" ], "dependencies": { + "@amplitude/analytics-react-native": "^1.6.8", "@expo/metro-config": "~57.0.0", "@expo/metro-runtime": "~57.0.7", "@expo/ui": "~57.0.7", @@ -168,6 +169,7 @@ "apps/mobile": { "version": "0.0.1", "dependencies": { + "@amplitude/analytics-react-native": "^1.6.8", "@expo/metro-config": "~57.0.0", "@expo/metro-runtime": "~57.0.7", "@expo/ui": "~57.0.7", @@ -301,6 +303,71 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@amplitude/analytics-connector": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.6.6.tgz", + "integrity": "sha512-gO+vLcCp2kYQoLYogzSOerzP7U1qPFdd75aFUxrOmkfAHMBy3GqOYKe9iebgFrVBLXnwHFmpFpr96eM/mLOlZQ==", + "license": "MIT" + }, + "node_modules/@amplitude/analytics-core": { + "version": "2.54.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.54.1.tgz", + "integrity": "sha512-VMpcIPw7MBM9glKwTZivMiPeUSysY06LZr0Rsdl35szJqKIl3JAuVPR8UnRyYOxqDUZfkOPIm9w1+3E/NNtWVw==", + "license": "MIT", + "dependencies": { + "@amplitude/analytics-connector": "^1.6.4", + "@types/zen-observable": "0.8.3", + "safe-json-stringify": "1.2.0", + "tslib": "^2.4.1", + "zen-observable": "0.10.0" + } + }, + "node_modules/@amplitude/analytics-react-native": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-react-native/-/analytics-react-native-1.6.8.tgz", + "integrity": "sha512-s/n5yKLcoobuj4u62D4M8VJGXOPrGR6RrnFfN5gO0uzDPQJNEeSoFDO6k4mPygMEox6MLVBfCXKt9Zk1xbujMA==", + "license": "MIT", + "dependencies": { + "@amplitude/analytics-core": "2.54.1", + "@amplitude/plugin-network-capture-browser": "1.10.12", + "@amplitude/ua-parser-js": "^0.7.31", + "@react-native-async-storage/async-storage": "^1.17.11", + "tslib": "^2.4.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/@amplitude/plugin-network-capture-browser": { + "version": "1.10.12", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.10.12.tgz", + "integrity": "sha512-FYdyMhPoToBL1GslOd7QjLctdhaL24sLKNGgXtYNVE8PiJNbmGNHWxf4Tr/CZ69uAuByXzo3Nvc0fGHpRIXiiQ==", + "license": "MIT", + "dependencies": { + "@amplitude/analytics-core": "2.54.1", + "tslib": "^2.4.1" + } + }, + "node_modules/@amplitude/ua-parser-js": { + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.33.tgz", + "integrity": "sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -8544,6 +8611,18 @@ } } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz", + "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.60 <1.0" + } + }, "node_modules/@react-native-community/hooks": { "version": "100.1.0", "resolved": "https://registry.npmjs.org/@react-native-community/hooks/-/hooks-100.1.0.tgz", @@ -11108,6 +11187,12 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "license": "MIT" }, + "node_modules/@types/zen-observable": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", + "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", @@ -24130,6 +24215,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-options/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -29520,6 +29626,12 @@ ], "license": "MIT" }, + "node_modules/safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -34074,6 +34186,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zen-observable": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.10.0.tgz", + "integrity": "sha512-iI3lT0iojZhKwT5DaFy2Ce42n3yFcLdFyOh01G7H0flMY60P8MJuVFEoJoNwXlmAyQ45GrjL6AcZmmlv8A5rbw==", + "license": "MIT" + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 7b53251..16d8fad 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "private": true, "dependencies": { + "@amplitude/analytics-react-native": "^1.6.8", "@expo/metro-config": "~57.0.0", "@expo/metro-runtime": "~57.0.7", "@expo/ui": "~57.0.7", From 03406f9620e936eb770c93cc8fb0c6336ec448bf Mon Sep 17 00:00:00 2001 From: vmikhalevskii Date: Wed, 5 Aug 2026 11:48:27 +0300 Subject: [PATCH 2/5] chore: add @react-native-async-storage/async-storage --- apps/mobile/package.json | 1 + package-lock.json | 22 ++++++++++++++++++---- package.json | 1 + 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index a9e2988..0e8238c 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -34,6 +34,7 @@ "@likashefqet/react-native-image-zoom": "^4.3.0", "@lukemorales/query-key-factory": "^1.3.4", "@pusher/pusher-websocket-react-native": "^1.3.1", + "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/hooks": "^100.1.0", "@react-native-community/netinfo": "12.0.1", "@ronas-it/axios-api-client": "^0.1.2", diff --git a/package-lock.json b/package-lock.json index 263cc25..b63861a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@likashefqet/react-native-image-zoom": "^4.3.0", "@lukemorales/query-key-factory": "^1.3.4", "@pusher/pusher-websocket-react-native": "^1.3.1", + "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/hooks": "^100.1.0", "@react-native-community/netinfo": "12.0.1", "@ronas-it/axios-api-client": "^0.1.2", @@ -179,6 +180,7 @@ "@likashefqet/react-native-image-zoom": "^4.3.0", "@lukemorales/query-key-factory": "^1.3.4", "@pusher/pusher-websocket-react-native": "^1.3.1", + "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/hooks": "^100.1.0", "@react-native-community/netinfo": "12.0.1", "@ronas-it/axios-api-client": "^0.1.2", @@ -339,6 +341,18 @@ "react-native": "*" } }, + "node_modules/@amplitude/analytics-react-native/node_modules/@react-native-async-storage/async-storage": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz", + "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.60 <1.0" + } + }, "node_modules/@amplitude/plugin-network-capture-browser": { "version": "1.10.12", "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.10.12.tgz", @@ -8612,15 +8626,15 @@ } }, "node_modules/@react-native-async-storage/async-storage": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz", - "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", "license": "MIT", "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { - "react-native": "^0.0.0-0 || >=0.60 <1.0" + "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "node_modules/@react-native-community/hooks": { diff --git a/package.json b/package.json index 16d8fad..b42fa24 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@likashefqet/react-native-image-zoom": "^4.3.0", "@lukemorales/query-key-factory": "^1.3.4", "@pusher/pusher-websocket-react-native": "^1.3.1", + "@react-native-async-storage/async-storage": "2.2.0", "@react-native-community/hooks": "^100.1.0", "@react-native-community/netinfo": "12.0.1", "@ronas-it/axios-api-client": "^0.1.2", From 6656ee2b050ef4fdf8cb63555fe2c2ad7a1fef9d Mon Sep 17 00:00:00 2001 From: vmikhalevskii Date: Wed, 5 Aug 2026 15:38:02 +0300 Subject: [PATCH 3/5] feat: add analytics service --- apps/mobile/.env.template | 8 ++-- apps/mobile/app.config.ts | 4 ++ apps/mobile/app/_layout.tsx | 2 + apps/mobile/nativewind-env.d.ts | 1 + .../shared/features/use-logout/src/hook.ts | 2 + .../features/use-init-requests/src/hook.ts | 10 ++++- libs/shared/utils/analytics-service/.babelrc | 12 ++++++ libs/shared/utils/analytics-service/README.md | 7 ++++ .../utils/analytics-service/eslint.config.cjs | 12 ++++++ .../utils/analytics-service/project.json | 9 +++++ .../utils/analytics-service/src/config.ts | 10 +++++ .../utils/analytics-service/src/index.ts | 1 + .../utils/analytics-service/src/service.ts | 39 +++++++++++++++++++ .../utils/analytics-service/tsconfig.json | 17 ++++++++ .../utils/analytics-service/tsconfig.lib.json | 19 +++++++++ tsconfig.base.json | 1 + 16 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 libs/shared/utils/analytics-service/.babelrc create mode 100644 libs/shared/utils/analytics-service/README.md create mode 100644 libs/shared/utils/analytics-service/eslint.config.cjs create mode 100644 libs/shared/utils/analytics-service/project.json create mode 100644 libs/shared/utils/analytics-service/src/config.ts create mode 100644 libs/shared/utils/analytics-service/src/index.ts create mode 100644 libs/shared/utils/analytics-service/src/service.ts create mode 100644 libs/shared/utils/analytics-service/tsconfig.json create mode 100644 libs/shared/utils/analytics-service/tsconfig.lib.json diff --git a/apps/mobile/.env.template b/apps/mobile/.env.template index 3177b72..da8ec8d 100644 --- a/apps/mobile/.env.template +++ b/apps/mobile/.env.template @@ -1,9 +1,11 @@ -EXPO_PUBLIC_PRIJECT_ID= +EXPO_PUBLIC_PROJECT_ID= EXPO_PUBLIC_APP_ID= EXPO_PUBLIC_APP_OWNER= -EXPO_PIBLIC_APP_SCHEME= +EXPO_PUBLIC_APP_SCHEME= EXPO_PUBLIC_APP_NAME= EXPO_PUBLIC_APP_SLUG= EXPO_PUBLIC_IS_INTERNAL_RELEASE= -SENTRY_DSN= \ No newline at end of file +SENTRY_DSN= +AMPLITUDE_API_KEY_DEV= +AMPLITUDE_API_KEY_PROD= diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 4804c1c..16e2483 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -15,6 +15,10 @@ const createConfig = (): Omit & { extra: { eas: EASConfig } sentry: { dsn: process.env.SENTRY_DSN, }, + amplitude: { + apiKeyDev: process.env.AMPLITUDE_API_KEY_DEV, + apiKeyProd: process.env.AMPLITUDE_API_KEY_PROD, + }, env: appEnv.current, googleIosClientId: appEnv.select({ default: process.env.GOOGLE_IOS_CLIENT_ID_DEV, diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index efaf6d8..7148336 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -14,6 +14,7 @@ import { import { queryClient } from '@open-webui-react-native/shared/data-access/query-client'; import { useSocket } from '@open-webui-react-native/shared/data-access/websocket'; import { useNetworkConnection } from '@open-webui-react-native/shared/features/network'; +import { analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; import { constants } from '@open-webui-react-native/shared/utils/config'; import { setupReactotron } from '@open-webui-react-native/shared/utils/reactotron'; import { setLanguage } from '@ronas-it/react-native-common-modules/i18n'; @@ -56,6 +57,7 @@ const translations = { const useLanguage = setLanguage(translations, constants.defaultLocale); setupReactotron('open-web-ui'); +analyticsService.init(); // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); diff --git a/apps/mobile/nativewind-env.d.ts b/apps/mobile/nativewind-env.d.ts index 9583462..e1b556f 100644 --- a/apps/mobile/nativewind-env.d.ts +++ b/apps/mobile/nativewind-env.d.ts @@ -1,3 +1,4 @@ /// // NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind. +declare module '*.css'; diff --git a/libs/mobile/shared/features/use-logout/src/hook.ts b/libs/mobile/shared/features/use-logout/src/hook.ts index ce02dd8..4883af1 100644 --- a/libs/mobile/shared/features/use-logout/src/hook.ts +++ b/libs/mobile/shared/features/use-logout/src/hook.ts @@ -3,6 +3,7 @@ import { authApi } from '@open-webui-react-native/shared/data-access/api'; import { authState$ } from '@open-webui-react-native/shared/data-access/auth'; import { cookieService } from '@open-webui-react-native/shared/data-access/cookie'; import { queryPersister } from '@open-webui-react-native/shared/data-access/persist-query-storage'; +import { analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; export const useLogout = (): { logout: () => Promise; isLoading: boolean } => { const queryClient = useQueryClient(); @@ -17,6 +18,7 @@ export const useLogout = (): { logout: () => Promise; isLoading: boolean } authState$.logout(); queryClient.removeQueries(); cookieService.clearAll(); + analyticsService.resetUser(); await queryPersister.removeClient(); }; diff --git a/libs/shared/features/use-init-requests/src/hook.ts b/libs/shared/features/use-init-requests/src/hook.ts index 747e3bb..81ed3e3 100644 --- a/libs/shared/features/use-init-requests/src/hook.ts +++ b/libs/shared/features/use-init-requests/src/hook.ts @@ -1,4 +1,6 @@ +import { useEffect } from 'react'; import { appConfigurationApi, authApi, modelsApi, usersApi } from '@open-webui-react-native/shared/data-access/api'; +import { analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; interface UseInitRequestsResult { isLoading: boolean; @@ -6,11 +8,17 @@ interface UseInitRequestsResult { export const useInitRequests = (): UseInitRequestsResult => { const { isLoading: isModelsLoading } = modelsApi.useGetModels(); - const { isLoading: isProfileLoading } = authApi.useGetProfile(); + const { data: profile, isLoading: isProfileLoading } = authApi.useGetProfile(); const { isLoading: isUserSettingsLoading } = usersApi.useGetUserSettings(); const { isLoading: isConfigurationLoading } = appConfigurationApi.useGetAppConfiguration(); const isLoading = isModelsLoading || isProfileLoading || isUserSettingsLoading || isConfigurationLoading; + useEffect(() => { + if (profile?.id) { + analyticsService.setUser(profile.id); + } + }, [profile]); + return { isLoading }; }; diff --git a/libs/shared/utils/analytics-service/.babelrc b/libs/shared/utils/analytics-service/.babelrc new file mode 100644 index 0000000..1ea870e --- /dev/null +++ b/libs/shared/utils/analytics-service/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/libs/shared/utils/analytics-service/README.md b/libs/shared/utils/analytics-service/README.md new file mode 100644 index 0000000..d239793 --- /dev/null +++ b/libs/shared/utils/analytics-service/README.md @@ -0,0 +1,7 @@ +# shared/utils/analytics-service + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared/utils/analytics-service` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/utils/analytics-service/eslint.config.cjs b/libs/shared/utils/analytics-service/eslint.config.cjs new file mode 100644 index 0000000..87eef8a --- /dev/null +++ b/libs/shared/utils/analytics-service/eslint.config.cjs @@ -0,0 +1,12 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/react'], + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/libs/shared/utils/analytics-service/project.json b/libs/shared/utils/analytics-service/project.json new file mode 100644 index 0000000..503ab2f --- /dev/null +++ b/libs/shared/utils/analytics-service/project.json @@ -0,0 +1,9 @@ +{ + "name": "shared/utils/analytics-service", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/utils/analytics-service/src", + "projectType": "library", + "tags": ["app:shared", "scope:shared", "type:utils"], + "// targets": "to see all targets run: nx show project shared/utils/analytics-service --web", + "targets": {} +} diff --git a/libs/shared/utils/analytics-service/src/config.ts b/libs/shared/utils/analytics-service/src/config.ts new file mode 100644 index 0000000..2ff83f3 --- /dev/null +++ b/libs/shared/utils/analytics-service/src/config.ts @@ -0,0 +1,10 @@ +import Constants from 'expo-constants'; +import { appEnv } from '@open-webui-react-native/shared/utils/app-env'; + +const amplitudeConfig = Constants.expoConfig?.extra?.amplitude; + +export const amplitudeApiKey = appEnv.select({ + development: amplitudeConfig?.apiKeyDev, + staging: amplitudeConfig?.apiKeyDev, + production: amplitudeConfig?.apiKeyProd, +}); diff --git a/libs/shared/utils/analytics-service/src/index.ts b/libs/shared/utils/analytics-service/src/index.ts new file mode 100644 index 0000000..f78beab --- /dev/null +++ b/libs/shared/utils/analytics-service/src/index.ts @@ -0,0 +1 @@ +export * from './service'; diff --git a/libs/shared/utils/analytics-service/src/service.ts b/libs/shared/utils/analytics-service/src/service.ts new file mode 100644 index 0000000..4f2766e --- /dev/null +++ b/libs/shared/utils/analytics-service/src/service.ts @@ -0,0 +1,39 @@ +import { init, setUserId, reset, Types, track } from '@amplitude/analytics-react-native'; +import Constants from 'expo-constants'; +import { amplitudeApiKey } from './config'; + +class AnalyticsService { + public init(): void { + init(amplitudeApiKey as string, undefined, { + trackingOptions: { + adid: false, + appSetId: false, + carrier: false, + deviceManufacturer: false, + deviceModel: true, + ipAddress: true, + idfv: false, + language: true, + osName: true, + osVersion: true, + platform: true, + }, + logLevel: Types.LogLevel.Debug, + appVersion: Constants.expoConfig?.version, + }); + } + + public setUser(userId: string): void { + setUserId(userId); + } + + public resetUser(): void { + reset(); + } + + public trackEvent(eventName: string, eventProperties?: Record): void { + track(eventName, eventProperties); + } +} + +export const analyticsService = new AnalyticsService(); diff --git a/libs/shared/utils/analytics-service/tsconfig.json b/libs/shared/utils/analytics-service/tsconfig.json new file mode 100644 index 0000000..93614c4 --- /dev/null +++ b/libs/shared/utils/analytics-service/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ], + "extends": "../../../../tsconfig.base.json" +} diff --git a/libs/shared/utils/analytics-service/tsconfig.lib.json b/libs/shared/utils/analytics-service/tsconfig.lib.json new file mode 100644 index 0000000..45b2297 --- /dev/null +++ b/libs/shared/utils/analytics-service/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index dacf24d..7c830cb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -258,6 +258,7 @@ "libs/shared/features/use-init-requests/src/index.ts" ], "@open-webui-react-native/shared/utils/alert-service": ["libs/shared/utils/alert-service/src/index.ts"], + "@open-webui-react-native/shared/utils/analytics-service": ["libs/shared/utils/analytics-service/src/index.ts"], "@open-webui-react-native/shared/utils/app-env": ["libs/shared/utils/app-env/src/index.ts"], "@open-webui-react-native/shared/utils/config": ["libs/shared/utils/config/src/index.ts"], "@open-webui-react-native/shared/utils/date": ["libs/shared/utils/date/src/index.ts"], From ee26d34670952f6fc18d80698660fc2c53cf6e6c Mon Sep 17 00:00:00 2001 From: vmikhalevskii Date: Wed, 5 Aug 2026 16:45:10 +0300 Subject: [PATCH 4/5] feat: add analytics events --- libs/mobile/chat/features/chat/src/lib/component.tsx | 6 ++++++ .../chat/features/create-chat/src/lib/component.tsx | 7 +++++++ .../chat/features/form-chat-input/src/lib/component.tsx | 3 +++ .../features/upsert-folder-sheet/src/lib/component.tsx | 2 ++ .../utils/analytics-service/src/enums/analytics-event.ts | 8 ++++++++ libs/shared/utils/analytics-service/src/enums/index.ts | 1 + libs/shared/utils/analytics-service/src/index.ts | 1 + 7 files changed, 28 insertions(+) create mode 100644 libs/shared/utils/analytics-service/src/enums/analytics-event.ts create mode 100644 libs/shared/utils/analytics-service/src/enums/index.ts diff --git a/libs/mobile/chat/features/chat/src/lib/component.tsx b/libs/mobile/chat/features/chat/src/lib/component.tsx index e4129f8..432862e 100644 --- a/libs/mobile/chat/features/chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/component.tsx @@ -24,6 +24,7 @@ import { import { Role } from '@open-webui-react-native/shared/data-access/common'; import { useSubscribeToQueryCache } from '@open-webui-react-native/shared/data-access/query-client'; import { webSocketConfig, webSocketState$ } from '@open-webui-react-native/shared/data-access/websocket'; +import { AnalyticsEvent, analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; import { useAppStateChange } from '@open-webui-react-native/shared/utils/use-app-state-change'; import { ActiveInputMode } from './enums'; @@ -169,7 +170,12 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C return ToastService.showError(translate('TEXT_MODEL_NOT_SELECTED')); } + if (options.includes(ChatGenerationOption.IMAGE_GENERATION)) { + analyticsService.trackEvent(AnalyticsEvent.GENERATE_IMAGE_USED); + } + sendMessage(inputValue, selectedModelId, options, attachedFiles.get(), attachedImages.get()); + analyticsService.trackEvent(AnalyticsEvent.MESSAGE_SENT, { modelId: selectedModelId }); reset(); resetAttachments(); // NOTE: Forces input rerender to reset it to its initial height after submit diff --git a/libs/mobile/chat/features/create-chat/src/lib/component.tsx b/libs/mobile/chat/features/create-chat/src/lib/component.tsx index b2f4dd9..5e162af 100644 --- a/libs/mobile/chat/features/create-chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/create-chat/src/lib/component.tsx @@ -11,6 +11,7 @@ import { View } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; import { FormValues } from '@open-webui-react-native/mobile/shared/utils/form'; import { ChatGenerationOption } from '@open-webui-react-native/shared/data-access/api'; import { webSocketState$ } from '@open-webui-react-native/shared/data-access/websocket'; +import { AnalyticsEvent, analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; import { SearchFolderView, SearchModelView } from './components'; @@ -43,6 +44,7 @@ export function CreateChat({ reset(); resetAttachments(); onChatCreated(id); + analyticsService.trackEvent(AnalyticsEvent.NEW_CHAT_CREATED); }; const { startChatCreation, isLoading: isCreating } = useCreateNewChat({ onSuccess: handleChatCreated }); @@ -62,7 +64,12 @@ export function CreateChat({ return ToastService.showError(translate('TEXT_MODEL_NOT_SELECTED')); } + if (options.includes(ChatGenerationOption.IMAGE_GENERATION)) { + analyticsService.trackEvent(AnalyticsEvent.GENERATE_IMAGE_USED); + } + startChatCreation(inputValue, modelId, options, attachedFiles.get(), attachedImages.get(), folderId); + analyticsService.trackEvent(AnalyticsEvent.MESSAGE_SENT, { modelId }); })(); useEffect(() => { diff --git a/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx b/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx index 6223b29..ba05d05 100644 --- a/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx +++ b/libs/mobile/chat/features/form-chat-input/src/lib/component.tsx @@ -22,6 +22,7 @@ import { } from '@open-webui-react-native/shared/data-access/api'; import { AttachedImage, FileData, ImageData } from '@open-webui-react-native/shared/data-access/common'; import { withOfflineGuard } from '@open-webui-react-native/shared/features/network'; +import { AnalyticsEvent, analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; import { FeatureID, isFeatureEnabled } from '@open-webui-react-native/shared/utils/feature-flag'; import { toDataUrl } from '@open-webui-react-native/shared/utils/files'; import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; @@ -111,6 +112,7 @@ export function FormChatInput({ const handleSuggestionPress = (suggestion: string): void => { field.onChange(suggestion); + analyticsService.trackEvent(AnalyticsEvent.SUGGESTION_USED); }; const onGenerationOptionPress = (option: ChatGenerationOption): void => setOptions((state) => xor(state, [option])); @@ -125,6 +127,7 @@ export function FormChatInput({ const onCompleteRecording = (text: string): void => { setIsDictateMode(false); field.onChange(text); + analyticsService.trackEvent(AnalyticsEvent.DICTATION_MODE_USED); }; const onStopGenerationPress = async (): Promise => { diff --git a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx b/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx index 1a68db4..9f4f76d 100644 --- a/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx +++ b/libs/mobile/folder/features/upsert-folder-sheet/src/lib/component.tsx @@ -31,6 +31,7 @@ import { prepareCreateFolderPayload, } from '@open-webui-react-native/shared/data-access/api'; import { AttachedFile, FileData, FileType } from '@open-webui-react-native/shared/data-access/common'; +import { AnalyticsEvent, analyticsService } from '@open-webui-react-native/shared/utils/analytics-service'; import { FeatureID, isFeatureEnabled } from '@open-webui-react-native/shared/utils/feature-flag'; import { getDocumentFormData } from '@open-webui-react-native/shared/utils/files'; import { ToastService } from '@open-webui-react-native/shared/utils/toast-service'; @@ -135,6 +136,7 @@ export function UpsertFolderSheet({ ref, ...props }: UpsertFolderSheetProps): Re }); } else { await createFolder(payload); + analyticsService.trackEvent(AnalyticsEvent.NEW_FOLDER_CREATED); } closeModal(); resetKnowledge(); diff --git a/libs/shared/utils/analytics-service/src/enums/analytics-event.ts b/libs/shared/utils/analytics-service/src/enums/analytics-event.ts new file mode 100644 index 0000000..dd9ac4e --- /dev/null +++ b/libs/shared/utils/analytics-service/src/enums/analytics-event.ts @@ -0,0 +1,8 @@ +export enum AnalyticsEvent { + NEW_CHAT_CREATED = 'new_chat_created', + NEW_FOLDER_CREATED = 'new_folder_created', + GENERATE_IMAGE_USED = 'generate_image_used', + DICTATION_MODE_USED = 'dictation_mode_used', + SUGGESTION_USED = 'suggestion_used', + MESSAGE_SENT = 'message_sent', +} diff --git a/libs/shared/utils/analytics-service/src/enums/index.ts b/libs/shared/utils/analytics-service/src/enums/index.ts new file mode 100644 index 0000000..50d8823 --- /dev/null +++ b/libs/shared/utils/analytics-service/src/enums/index.ts @@ -0,0 +1 @@ +export * from './analytics-event'; diff --git a/libs/shared/utils/analytics-service/src/index.ts b/libs/shared/utils/analytics-service/src/index.ts index f78beab..1638a1c 100644 --- a/libs/shared/utils/analytics-service/src/index.ts +++ b/libs/shared/utils/analytics-service/src/index.ts @@ -1 +1,2 @@ export * from './service'; +export * from './enums'; From 78e3644fe3cc09544544b0e71d209cc3323d1265 Mon Sep 17 00:00:00 2001 From: vmikhalevskii Date: Fri, 7 Aug 2026 12:10:56 +0300 Subject: [PATCH 5/5] fix: correct server zone --- libs/shared/utils/analytics-service/src/service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/shared/utils/analytics-service/src/service.ts b/libs/shared/utils/analytics-service/src/service.ts index 4f2766e..4efd0f9 100644 --- a/libs/shared/utils/analytics-service/src/service.ts +++ b/libs/shared/utils/analytics-service/src/service.ts @@ -5,6 +5,7 @@ import { amplitudeApiKey } from './config'; class AnalyticsService { public init(): void { init(amplitudeApiKey as string, undefined, { + serverZone: 'EU', trackingOptions: { adid: false, appSetId: false,