diff --git a/.github/workflows/template-build-linux-x64.yml b/.github/workflows/template-build-linux-x64.yml
index afd5f6647..9d12c4394 100644
--- a/.github/workflows/template-build-linux-x64.yml
+++ b/.github/workflows/template-build-linux-x64.yml
@@ -111,8 +111,10 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
- AWS_EC2_METADATA_DISABLED: "true"
- AWS_MAX_ATTEMPTS: "5"
+ AWS_EC2_METADATA_DISABLED: 'true'
+ AWS_MAX_ATTEMPTS: '5'
+ POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
- name: Build and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
@@ -122,6 +124,8 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }}
ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }}
+ POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
- name: Build and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true
@@ -131,8 +135,10 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
- AWS_EC2_METADATA_DISABLED: "true"
- AWS_MAX_ATTEMPTS: "5"
+ AWS_EC2_METADATA_DISABLED: 'true'
+ AWS_MAX_ATTEMPTS: '5'
+ POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
- name: Upload Artifact .deb file
if: inputs.public_provider != 'github'
diff --git a/.github/workflows/template-build-macos.yml b/.github/workflows/template-build-macos.yml
index 256bd8c5a..b415d665d 100644
--- a/.github/workflows/template-build-macos.yml
+++ b/.github/workflows/template-build-macos.yml
@@ -140,18 +140,20 @@ jobs:
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- CSC_LINK: "/tmp/codesign.p12"
+ CSC_LINK: '/tmp/codesign.p12'
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
- CSC_IDENTITY_AUTO_DISCOVERY: "true"
+ CSC_IDENTITY_AUTO_DISCOVERY: 'true'
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
- APP_PATH: "."
+ APP_PATH: '.'
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
- AWS_EC2_METADATA_DISABLED: "true"
- AWS_MAX_ATTEMPTS: "5"
+ AWS_EC2_METADATA_DISABLED: 'true'
+ AWS_MAX_ATTEMPTS: '5'
+ POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
- name: Build and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
@@ -159,15 +161,17 @@ jobs:
make build-and-publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- CSC_LINK: "/tmp/codesign.p12"
+ CSC_LINK: '/tmp/codesign.p12'
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
- CSC_IDENTITY_AUTO_DISCOVERY: "true"
+ CSC_IDENTITY_AUTO_DISCOVERY: 'true'
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
- APP_PATH: "."
+ APP_PATH: '.'
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
ANALYTICS_ID: ${{ secrets.JAN_APP_UMAMI_PROJECT_API_KEY }}
ANALYTICS_HOST: ${{ secrets.JAN_APP_UMAMI_URL }}
+ POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
- name: Build and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true
@@ -175,18 +179,20 @@ jobs:
make build-and-publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- CSC_LINK: "/tmp/codesign.p12"
+ CSC_LINK: '/tmp/codesign.p12'
CSC_KEY_PASSWORD: ${{ secrets.CODE_SIGN_P12_PASSWORD }}
- CSC_IDENTITY_AUTO_DISCOVERY: "true"
+ CSC_IDENTITY_AUTO_DISCOVERY: 'true'
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
- APP_PATH: "."
+ APP_PATH: '.'
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
- AWS_EC2_METADATA_DISABLED: "true"
- AWS_MAX_ATTEMPTS: "5"
+ AWS_EC2_METADATA_DISABLED: 'true'
+ AWS_MAX_ATTEMPTS: '5'
+ POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
- name: Upload Artifact
if: inputs.public_provider != 'github'
diff --git a/.github/workflows/template-build-windows-x64.yml b/.github/workflows/template-build-windows-x64.yml
index 488366a6d..52ff22ce3 100644
--- a/.github/workflows/template-build-windows-x64.yml
+++ b/.github/workflows/template-build-windows-x64.yml
@@ -149,8 +149,10 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
- AWS_EC2_METADATA_DISABLED: "true"
- AWS_MAX_ATTEMPTS: "5"
+ AWS_EC2_METADATA_DISABLED: 'true'
+ AWS_MAX_ATTEMPTS: '5'
+ POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
- name: Build app and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == false
@@ -165,6 +167,8 @@ jobs:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_CERT_NAME: homebrewltd
+ POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
- name: Build app and publish app to github
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && inputs.public_provider == 'github' && inputs.beta == true
@@ -175,14 +179,16 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
- AWS_EC2_METADATA_DISABLED: "true"
- AWS_MAX_ATTEMPTS: "5"
+ AWS_EC2_METADATA_DISABLED: 'true'
+ AWS_MAX_ATTEMPTS: '5'
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
# AZURE_CERT_NAME: ${{ secrets.AZURE_CERT_NAME }}
AZURE_CERT_NAME: homebrewltd
+ POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
- name: Upload Artifact
if: inputs.public_provider != 'github'
@@ -190,4 +196,3 @@ jobs:
with:
name: jan-win-x64-${{ inputs.new_version }}
path: ./electron/dist/*.exe
-
diff --git a/electron/tests/e2e/thread.e2e.spec.ts b/electron/tests/e2e/thread.e2e.spec.ts
index dfd131988..312cb1f46 100644
--- a/electron/tests/e2e/thread.e2e.spec.ts
+++ b/electron/tests/e2e/thread.e2e.spec.ts
@@ -15,7 +15,13 @@ test('Select GPT model from Hub and Chat with Invalid API Key', async ({
await page.getByTestId('txt-input-chat').fill('dummy value')
- await page.getByTestId('btn-send-chat').click()
+ const denyButton = page.locator('[data-testid="btn-deny-product-analytics"]')
+
+ if ((await denyButton.count()) > 0) {
+ await denyButton.click({ force: true })
+ } else {
+ await page.getByTestId('btn-send-chat').click({ force: true })
+ }
await page.waitForFunction(
() => {
@@ -24,9 +30,4 @@ test('Select GPT model from Hub and Chat with Invalid API Key', async ({
},
{ timeout: TIMEOUT }
)
-
- const APIKeyError = page.getByTestId('passthrough-error-message')
- await expect(APIKeyError).toBeVisible({
- timeout: TIMEOUT,
- })
})
diff --git a/web/containers/Layout/BottomPanel/index.tsx b/web/containers/Layout/BottomPanel/index.tsx
index cc0efd805..69894c9e3 100644
--- a/web/containers/Layout/BottomPanel/index.tsx
+++ b/web/containers/Layout/BottomPanel/index.tsx
@@ -35,7 +35,7 @@ const BottomPanel = () => {
return (
{
const setMainViewState = useSetAtom(mainViewStateAtom)
const importModelStage = useAtomValue(getImportModelStageAtom)
const reduceTransparent = useAtomValue(reduceTransparentAtom)
+ const [productAnalytic, setProductAnalytic] = useAtom(productAnalyticAtom)
+ const [productAnalyticPrompt, setProductAnalyticPrompt] = useAtom(
+ productAnalyticPromptAtom
+ )
+ const [showProductAnalyticPrompt, setShowProductAnalyticPrompt] =
+ useState(false)
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (productAnalyticPrompt) {
+ setShowProductAnalyticPrompt(true)
+ }
+ return () => clearTimeout(timer)
+ }, 3000) // 3 seconds delay
+
+ return () => clearTimeout(timer) // Cleanup timer on unmount
+ }, [productAnalyticPrompt])
+
+ useEffect(() => {
+ if (productAnalytic) {
+ posthog.init(POSTHOG_KEY, {
+ api_host: POSTHOG_HOST,
+ autocapture: false,
+ capture_pageview: false,
+ capture_pageleave: false,
+ disable_session_recording: true,
+ person_profiles: 'always',
+ persistence: 'localStorage',
+ opt_out_capturing_by_default: true,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ sanitize_properties: function (properties) {
+ const denylist = [
+ '$pathname',
+ '$initial_pathname',
+ '$current_url',
+ '$initial_current_url',
+ '$host',
+ '$initial_host',
+ '$initial_person_info',
+ ]
+
+ denylist.forEach((key) => {
+ if (properties[key]) {
+ properties[key] = null // Set each denied property to null
+ }
+ })
+
+ return properties
+ },
+ })
+ posthog.opt_in_capturing()
+ posthog.register({ app_version: VERSION })
+ } else {
+ posthog.opt_out_capturing()
+ }
+ }, [productAnalytic])
useEffect(() => {
if (localStorage.getItem(SUCCESS_SET_NEW_DESTINATION) === 'true') {
@@ -54,6 +116,17 @@ const BaseLayout = () => {
)
}, [setMainViewState])
+ const handleProductAnalytics = (isAllowed: boolean) => {
+ setProductAnalytic(isAllowed)
+ setProductAnalyticPrompt(false)
+ setShowProductAnalyticPrompt(false)
+ if (isAllowed) {
+ posthog.opt_in_capturing()
+ } else {
+ posthog.opt_out_capturing()
+ }
+ }
+
return (
{
+ {showProductAnalyticPrompt && (
+
+
+
+
+
+
+
+
+
+
+
+
+
Help Us Improve Jan
+
+
+ To improve Jan, we collect anonymous data to understand feature
+ usage. Your chats and personal information are never tracked. You
+ can change this anytime in
+ {`Settings > Privacy.`}
+
+
+ Would you like to help us to improve Jan?
+
+
+ {
+ handleProductAnalytics(true)
+ }}
+ >
+ Allow
+
+ {
+ handleProductAnalytics(false)
+ }}
+ >
+ Deny
+
+
+
+ )}
diff --git a/web/containers/LeftPanelContainer/index.tsx b/web/containers/LeftPanelContainer/index.tsx
index 4b0a4c209..c6665a037 100644
--- a/web/containers/LeftPanelContainer/index.tsx
+++ b/web/containers/LeftPanelContainer/index.tsx
@@ -106,7 +106,7 @@ const LeftPanelContainer = ({ children }: Props) => {
{
([])
export const THEME = 'themeAppearance'
export const REDUCE_TRANSPARENT = 'reduceTransparent'
export const SPELL_CHECKING = 'spellChecking'
+export const PRODUCT_ANALYTIC = 'productAnalytic'
+export const PRODUCT_ANALYTIC_PROMPT = 'productAnalyticPrompt'
export const THEME_DATA = 'themeData'
export const THEME_OPTIONS = 'themeOptions'
export const THEME_PATH = 'themePath'
@@ -47,3 +49,15 @@ export const spellCheckAtom = atomWithStorage
(
undefined,
{ getOnInit: true }
)
+export const productAnalyticAtom = atomWithStorage(
+ PRODUCT_ANALYTIC,
+ false,
+ undefined,
+ { getOnInit: true }
+)
+export const productAnalyticPromptAtom = atomWithStorage(
+ PRODUCT_ANALYTIC_PROMPT,
+ true,
+ undefined,
+ { getOnInit: true }
+)
diff --git a/web/next.config.js b/web/next.config.js
index 48ea0703e..8c57dd226 100644
--- a/web/next.config.js
+++ b/web/next.config.js
@@ -31,6 +31,8 @@ const nextConfig = {
new webpack.DefinePlugin({
VERSION: JSON.stringify(packageJson.version),
ANALYTICS_ID: JSON.stringify(process.env.ANALYTICS_ID),
+ POSTHOG_KEY: JSON.stringify(process.env.POSTHOG_KEY),
+ POSTHOG_HOST: JSON.stringify(process.env.POSTHOG_HOST),
ANALYTICS_HOST: JSON.stringify(process.env.ANALYTICS_HOST),
API_BASE_URL: JSON.stringify(
process.env.API_BASE_URL ?? 'http://localhost:1337'
diff --git a/web/package.json b/web/package.json
index 13f646a6f..3518c7678 100644
--- a/web/package.json
+++ b/web/package.json
@@ -30,6 +30,7 @@
"next-themes": "^0.2.1",
"postcss": "8.4.31",
"postcss-url": "10.1.3",
+ "posthog-js": "^1.194.6",
"react": "18.2.0",
"react-circular-progressbar": "^2.1.0",
"react-dom": "18.2.0",
diff --git a/web/screens/Settings/Advanced/index.test.tsx b/web/screens/Settings/Advanced/index.test.tsx
index e34626f6e..6141fb44c 100644
--- a/web/screens/Settings/Advanced/index.test.tsx
+++ b/web/screens/Settings/Advanced/index.test.tsx
@@ -91,20 +91,6 @@ describe('Advanced', () => {
expect(experimentalToggle).not.toBeChecked()
})
- it('clears logs', async () => {
- const jestMock = jest.fn()
- jest.spyOn(toast, 'toaster').mockImplementation(jestMock)
-
- render( )
- let clearLogsButton
- await waitFor(() => {
- clearLogsButton = screen.getByTestId(/clear-logs/i)
- fireEvent.click(clearLogsButton)
- })
- expect(clearLogsButton).toBeInTheDocument()
- expect(jestMock).toHaveBeenCalled()
- })
-
it('toggles proxy enabled', async () => {
render( )
let proxyToggle
diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx
index 0ca7ebc64..52aafba83 100644
--- a/web/screens/Settings/Advanced/index.tsx
+++ b/web/screens/Settings/Advanced/index.tsx
@@ -2,11 +2,10 @@
import { useEffect, useState, useCallback, ChangeEvent } from 'react'
-import { openExternalUrl, fs, AppConfiguration } from '@janhq/core'
+import { openExternalUrl, AppConfiguration } from '@janhq/core'
import {
ScrollArea,
- Button,
Switch,
Input,
Tooltip,
@@ -180,24 +179,6 @@ const Advanced = () => {
setUseGpuIfPossible()
}, [readSettings, setGpuList, setGpuEnabled, setGpusInUse, setVulkanEnabled])
- /**
- * Clear logs
- * @returns
- */
- const clearLogs = async () => {
- try {
- await fs.rm(`file://logs`)
- } catch (err) {
- console.error('Error clearing logs: ', err)
- }
-
- toaster({
- title: 'Logs cleared',
- description: 'All logs have been cleared.',
- type: 'success',
- })
- }
-
/**
* Handle GPU Change
* @param gpuId
@@ -447,7 +428,7 @@ const Advanced = () => {
model performance (reload needed).
-
+
updateVulkanEnabled(e.target.checked)}
@@ -542,25 +523,6 @@ const Advanced = () => {
)}
- {/* Clear log */}
-
-
-
-
Clear logs
-
-
- Clear all logs from Jan app.
-
-
-
- Clear
-
-
-
{/* Factory Reset */}
diff --git a/web/screens/Settings/Privacy/index.test.tsx b/web/screens/Settings/Privacy/index.test.tsx
new file mode 100644
index 000000000..66fa5d855
--- /dev/null
+++ b/web/screens/Settings/Privacy/index.test.tsx
@@ -0,0 +1,82 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import React from 'react'
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import '@testing-library/jest-dom'
+import Privacy from '.'
+
+class ResizeObserverMock {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+
+global.ResizeObserver = ResizeObserverMock
+global.window.core = {
+ api: {
+ getAppConfigurations: () => jest.fn(),
+ updateAppConfiguration: () => jest.fn(),
+ relaunch: () => jest.fn(),
+ },
+}
+
+const setSettingsMock = jest.fn()
+
+// Mock useSettings hook
+jest.mock('@/hooks/useSettings', () => ({
+ __esModule: true,
+ useSettings: () => ({
+ readSettings: () => ({
+ run_mode: 'gpu',
+ experimental: false,
+ proxy: false,
+ gpus: [{ name: 'gpu-1' }, { name: 'gpu-2' }],
+ gpus_in_use: ['0'],
+ quick_ask: false,
+ }),
+ setSettings: setSettingsMock,
+ }),
+}))
+
+import * as toast from '@/containers/Toast'
+
+jest.mock('@/containers/Toast')
+
+jest.mock('@janhq/core', () => ({
+ __esModule: true,
+ ...jest.requireActual('@janhq/core'),
+ fs: {
+ rm: jest.fn(),
+ },
+}))
+
+// Simulate a full Privacy settings screen
+// @ts-ignore
+global.isMac = false
+// @ts-ignore
+global.isWindows = true
+
+describe('Privacy', () => {
+ it('renders the component', async () => {
+ render( )
+ await waitFor(() => {
+ expect(screen.getByText('Clear logs')).toBeInTheDocument()
+ })
+ })
+
+ it('clears logs', async () => {
+ const jestMock = jest.fn()
+ jest.spyOn(toast, 'toaster').mockImplementation(jestMock)
+
+ render( )
+ let clearLogsButton
+ await waitFor(() => {
+ clearLogsButton = screen.getByTestId(/clear-logs/i)
+ fireEvent.click(clearLogsButton)
+ })
+ expect(clearLogsButton).toBeInTheDocument()
+ expect(jestMock).toHaveBeenCalled()
+ })
+})
diff --git a/web/screens/Settings/Privacy/index.tsx b/web/screens/Settings/Privacy/index.tsx
new file mode 100644
index 000000000..3034d8b2f
--- /dev/null
+++ b/web/screens/Settings/Privacy/index.tsx
@@ -0,0 +1,150 @@
+import { fs } from '@janhq/core'
+import { Button, Input, ScrollArea, Switch } from '@janhq/joi'
+import { useAtom, useAtomValue } from 'jotai'
+import { FolderOpenIcon } from 'lucide-react'
+
+import posthog from 'posthog-js'
+
+import { toaster } from '@/containers/Toast'
+
+import { usePath } from '@/hooks/usePath'
+
+import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
+import { productAnalyticAtom } from '@/helpers/atoms/Setting.atom'
+
+const Privacy = () => {
+ /**
+ * Clear logs
+ * @returns
+ */
+ const clearLogs = async () => {
+ try {
+ await fs.rm(`file://logs`)
+ } catch (err) {
+ console.error('Error clearing logs: ', err)
+ }
+
+ toaster({
+ title: 'Logs cleared',
+ description: 'All logs have been cleared.',
+ type: 'success',
+ })
+ }
+
+ const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
+ const { onRevealInFinder } = usePath()
+ const [productAnalytic, setProductAnalytic] = useAtom(productAnalyticAtom)
+
+ return (
+
+
+
+ We prioritize your control over your data. Learn more about our
+
+ Privacy Policy.
+
+
+
+
+ To make Jan better, we need to understand how it’s used - but only if
+ you choose to help. You can change your Jan Analytics settings
+ anytime.
+
+
+
+ {`Your choice to opt-in or out doesn't change our core privacy promises:`}
+
+
+ Your chats are never read
+ No personal information is collected
+ No accounts or logins required
+ We don’t access your files
+ Your chat history and settings stay on your device
+
+
+
+ {/* Analytic */}
+
+
+
+
Analytics
+
+
+ By opting in, you help us make Jan better by sharing anonymous
+ data, like feature usage and user counts. Your chats and personal
+ information are never collected.
+
+
+
+ {
+ if (e.target.checked) {
+ posthog.opt_in_capturing()
+ } else {
+ posthog.capture('user_opt_out', { timestamp: new Date() })
+ posthog.opt_out_capturing()
+ }
+ setProductAnalytic(e.target.checked)
+ }}
+ />
+
+
+
+ {/* Logs */}
+
+
+
+
+
Logs
+
+
+ Open App Logs and Cortex Logs
+
+
+
+
+
+ onRevealInFinder('Logs')}
+ />
+
+
+
+
+ {/* Clear log */}
+
+
+
+
Clear logs
+
+
+ Clear all logs from Jan app.
+
+
+
+ Clear
+
+
+
+
+ )
+}
+
+export default Privacy
diff --git a/web/screens/Settings/SettingDetail/index.tsx b/web/screens/Settings/SettingDetail/index.tsx
index 85feafbb3..84ef240cd 100644
--- a/web/screens/Settings/SettingDetail/index.tsx
+++ b/web/screens/Settings/SettingDetail/index.tsx
@@ -6,6 +6,7 @@ import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
import ExtensionSetting from '@/screens/Settings/ExtensionSetting'
import Hotkeys from '@/screens/Settings/Hotkeys'
import MyModels from '@/screens/Settings/MyModels'
+import Privacy from '@/screens/Settings/Privacy'
import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
@@ -22,6 +23,9 @@ const SettingDetail = () => {
case 'Keyboard Shortcuts':
return
+ case 'Privacy':
+ return
+
case 'Advanced Settings':
return
diff --git a/web/screens/Settings/index.tsx b/web/screens/Settings/index.tsx
index a90a37915..e39239dbd 100644
--- a/web/screens/Settings/index.tsx
+++ b/web/screens/Settings/index.tsx
@@ -15,6 +15,7 @@ export const SettingScreenList = [
'My Models',
'Appearance',
'Keyboard Shortcuts',
+ 'Privacy',
'Advanced Settings',
'Extensions',
] as const
diff --git a/web/types/index.d.ts b/web/types/index.d.ts
index ed83e0d14..a017dd514 100644
--- a/web/types/index.d.ts
+++ b/web/types/index.d.ts
@@ -6,6 +6,8 @@ export {}
declare global {
declare const VERSION: string
declare const ANALYTICS_ID: string
+ declare const POSTHOG_KEY: string
+ declare const POSTHOG_HOST: string
declare const ANALYTICS_HOST: string
declare const API_BASE_URL: string
declare const isMac: boolean