diff --git a/apps/mobile/package.json b/apps/mobile/package.json index ca8c784a502..04dbc1f4d76 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -51,6 +51,7 @@ "expo-constants": "~57.0.7", "expo-crypto": "~57.0.1", "expo-dev-client": "~57.0.9", + "expo-device": "~57.0.1", "expo-document-picker": "~57.0.1", "expo-file-system": "~57.0.1", "expo-font": "57.0.1", diff --git a/apps/mobile/src/lib/analytics/posthog.test.ts b/apps/mobile/src/lib/analytics/posthog.test.ts new file mode 100644 index 00000000000..3ce9c8239e6 --- /dev/null +++ b/apps/mobile/src/lib/analytics/posthog.test.ts @@ -0,0 +1,106 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const hoisted = vi.hoisted(() => { + const client = { + register: vi.fn(), + setPersonPropertiesForFlags: vi.fn(), + onFeatureFlags: vi.fn(), + }; + const holder: { options?: Record } = {}; + const device: { deviceType: number | null } = { deviceType: null }; + return { client, holder, device }; +}); + +vi.mock('posthog-react-native', () => ({ + // Must be a `function`, not an arrow: arrows are not constructible and + // `new PostHog(...)` throws "not a constructor". A `function` returning an + // object makes `new` yield that object. + default: vi.fn(function PostHogMock(_key: string, options: Record) { + hoisted.holder.options = options; + return hoisted.client; + }), +})); + +vi.mock('expo-device', () => ({ + DeviceType: { UNKNOWN: 0, PHONE: 1, TABLET: 2, DESKTOP: 3, TV: 4 }, + // Getter: reads the holder at access time, so per-test values take effect. + get deviceType() { + return hoisted.device.deviceType; + }, +})); + +// Required: the real expo-application import pulls in react-native, which +// pure vitest cannot load. +vi.mock('expo-application', () => ({ + nativeApplicationVersion: '1.2.3', + nativeBuildVersion: '45', +})); + +vi.mock('@/lib/config', () => ({ POSTHOG_API_KEY: 'test-key' })); + +vi.stubGlobal('__DEV__', false); + +type CustomAppProperties = (properties: Record) => Record; + +function readCustomAppProperties(): CustomAppProperties { + const customAppProperties = hoisted.holder.options?.customAppProperties; + expect(customAppProperties).toEqual(expect.any(Function)); + return customAppProperties as CustomAppProperties; +} + +async function loadInitPostHog() { + vi.resetModules(); + const module = await import('./posthog'); + return module.initPostHog; +} + +describe('initPostHog device_form_factor', () => { + beforeEach(() => { + vi.clearAllMocks(); + hoisted.holder.options = undefined; + hoisted.device.deviceType = null; + }); + + it.each([ + { deviceType: 1, expected: 'phone' }, + { deviceType: 2, expected: 'tablet' }, + { deviceType: 3, expected: 'desktop' }, + { deviceType: 4, expected: 'tv' }, + { deviceType: 0, expected: 'unknown' }, + { deviceType: null, expected: 'unknown' }, + ] as const)( + 'maps deviceType $deviceType to device_form_factor $expected', + async ({ deviceType, expected }) => { + hoisted.device.deviceType = deviceType; + const initPostHog = await loadInitPostHog(); + initPostHog(); + + const customAppProperties = readCustomAppProperties(); + expect(customAppProperties({ $device_type: 'Mobile' })).toEqual({ + $device_type: 'Mobile', + device_form_factor: expected, + }); + } + ); + + it('registers platform: mobile once', async () => { + hoisted.device.deviceType = 1; + const initPostHog = await loadInitPostHog(); + initPostHog(); + + expect(hoisted.client.register).toHaveBeenCalledTimes(1); + expect(hoisted.client.register).toHaveBeenCalledWith({ platform: 'mobile' }); + }); + + it('constructs PostHog only once when init is called twice', async () => { + hoisted.device.deviceType = 1; + const initPostHog = await loadInitPostHog(); + const posthogModule = await import('posthog-react-native'); + const PostHog = posthogModule.default; + + initPostHog(); + initPostHog(); + + expect(PostHog).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/mobile/src/lib/analytics/posthog.ts b/apps/mobile/src/lib/analytics/posthog.ts index 68f95c89bc2..f59c4c75d89 100644 --- a/apps/mobile/src/lib/analytics/posthog.ts +++ b/apps/mobile/src/lib/analytics/posthog.ts @@ -1,4 +1,5 @@ import * as Application from 'expo-application'; +import * as Device from 'expo-device'; import PostHog from 'posthog-react-native'; import { useCallback, useSyncExternalStore } from 'react'; @@ -10,7 +11,23 @@ import { POSTHOG_API_KEY } from '@/lib/config'; * the web convention (see apps/web PostHogProvider) — otherwise the same * person double-counts across platforms. * - * Payload rules (hard): stable enum strings only — no free text, no PII. + * Payload rules (hard): stable enum strings only — no free text, no PII. This + * binds OUR `captureEvent` / `customAppProperties` payloads. The SDK's stock + * auto-captured device fields (`$device_name`, `$device_manufacturer`, etc.) + * are an intentional exception — still no PII beyond stock device metadata. + * + * Auto-captured on every event (posthog-react-native 4.59.0 `native-deps.js`): + * - Once `expo-device` is installed: `$device_manufacturer`, `$device_name`, + * `$os_name`, `$os_version`, `$is_emulator` + * - Always: `$device_type` (always the string `'Mobile'` — no form-factor + * granularity), `$app_name`/`$app_version`/`$app_build`/`$app_namespace`, + * `$locale`/`$timezone`, `$screen_width`/`$screen_height` + * + * Brand (`Device.brand`) is skipped — near-duplicate of manufacturer, no extra + * segmentation value. Form factor is the real gap: we add `device_form_factor` + * via the `customAppProperties` constructor option (merges into `_appProperties`, + * lands on every event including the first, survives `client.reset()` — unlike + * `register()`). We do not override the reserved `$device_type`. */ export const SESSION_VIEWED_EVENT = 'session_viewed'; @@ -57,6 +74,26 @@ function appVersionProperties(): Record { return props; } +// Total mapping of expo-device DeviceType (+ null) → stable enum strings. +// Always present on every event via customAppProperties; never conditional. +function deviceFormFactor(): 'phone' | 'tablet' | 'desktop' | 'tv' | 'unknown' { + // Total mapping: null / UNKNOWN / unexpected → 'unknown'. if-chain (not + // switch) so exhaustiveness over DeviceType | null stays lint-clean. + if (Device.deviceType === Device.DeviceType.PHONE) { + return 'phone'; + } + if (Device.deviceType === Device.DeviceType.TABLET) { + return 'tablet'; + } + if (Device.deviceType === Device.DeviceType.DESKTOP) { + return 'desktop'; + } + if (Device.deviceType === Device.DeviceType.TV) { + return 'tv'; + } + return 'unknown'; +} + export function initPostHog(): void { if (client) { return; @@ -65,6 +102,10 @@ export function initPostHog(): void { host: 'https://us.i.posthog.com', // No events are sent from dev builds. disabled: __DEV__, + customAppProperties: properties => ({ + ...properties, + device_form_factor: deviceFormFactor(), + }), }); // Super property on every event so dashboards can filter mobile vs web // without relying on $lib. diff --git a/apps/mobile/vitest.pure.config.ts b/apps/mobile/vitest.pure.config.ts index 930a338d997..dd4cdad8be2 100644 --- a/apps/mobile/vitest.pure.config.ts +++ b/apps/mobile/vitest.pure.config.ts @@ -18,6 +18,7 @@ export default defineProject({ 'src/lib/*.test.ts', 'src/lib/a11y/**/*.test.ts', 'src/lib/agent-attachments/**/*.test.ts', + 'src/lib/analytics/**/*.test.ts', 'src/lib/auth/**/*.test.ts', 'src/lib/apple-iap/**/*.test.ts', 'src/lib/apple-iap/**/*.test.tsx', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fcbfd395849..afe560ca018 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -377,6 +377,9 @@ importers: expo-dev-client: specifier: ~57.0.9 version: 57.0.9(expo@57.0.8)(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-device: + specifier: ~57.0.1 + version: 57.0.1(expo@57.0.8) expo-document-picker: specifier: ~57.0.1 version: 57.0.1(expo@57.0.8) @@ -463,7 +466,7 @@ importers: version: 5.0.0-preview.4(react-native-css@3.0.7(@expo/metro-config@57.0.7(bufferutil@4.1.0)(expo@57.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6))(lightningcss@1.30.1)(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(tailwindcss@4.3.3) posthog-react-native: specifier: 4.59.0 - version: 4.59.0(expo-application@57.0.2(expo@57.0.8))(expo-file-system@57.0.1(expo@57.0.8)(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo-localization@57.0.1(expo@57.0.8)(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)) + version: 4.59.0(expo-application@57.0.2(expo@57.0.8))(expo-device@57.0.1(expo@57.0.8))(expo-file-system@57.0.1(expo@57.0.8)(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo-localization@57.0.1(expo@57.0.8)(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)) react: specifier: 19.2.3 version: 19.2.3 @@ -1268,7 +1271,7 @@ importers: version: 7.0.0-dev.20260514.1 jest: specifier: 30.3.0 - version: 30.3.0(@types/node@24.12.4)(node-notifier@10.0.1) + version: 30.3.0(@types/node@25.5.2)(node-notifier@10.0.1) typescript: specifier: 'catalog:' version: 5.9.3 @@ -1918,7 +1921,7 @@ importers: version: 0.3.7 '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@kilocode/sdk': specifier: 7.3.54 version: 7.3.54 @@ -2066,7 +2069,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/jest': specifier: 30.0.0 version: 30.0.0 @@ -2228,7 +2231,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/node': specifier: 'catalog:' version: 24.12.4 @@ -2403,7 +2406,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@typescript/native-preview': specifier: 'catalog:' version: 7.0.0-dev.20260514.1 @@ -2510,7 +2513,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/content-disposition': specifier: 0.5.9 version: 0.5.9 @@ -2867,7 +2870,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/node': specifier: 'catalog:' version: 24.12.4 @@ -2904,7 +2907,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/node': specifier: 'catalog:' version: 24.12.4 @@ -3134,7 +3137,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: 'catalog:' - version: 0.16.13(@cloudflare/workers-types@4.20260605.1)(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) + version: 0.16.13(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6) '@types/node': specifier: 'catalog:' version: 24.12.4 @@ -4601,11 +4604,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.hirok.io' + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.hirok.io' + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild/aix-ppc64@0.27.4': resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} @@ -12764,6 +12767,11 @@ packages: expo: '*' react-native: '*' + expo-device@57.0.1: + resolution: {integrity: sha512-jyEMDUticH+dhcL3GHa2aiifOvGXJsmb3oVT2R2q4i8bN7Bddy61+NkpMmuS2VAZrvoLQwf0TJJ/1vi1ukvutA==} + peerDependencies: + expo: '*' + expo-document-picker@57.0.1: resolution: {integrity: sha512-qBwM5oxDZ3I9kwFD3pUE1oK/WNv9artoEKO6UpqhQgNRr0XA1ALRVWYjkF4+ge9lUNDRehjTm/jenINkzqg84g==} peerDependencies: @@ -18269,6 +18277,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-parser-js@0.7.41: + resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -20945,6 +20957,21 @@ snapshots: - bufferutil - utf-8-validate + '@cloudflare/vitest-pool-workers@0.16.13(@vitest/runner@4.1.6)(@vitest/snapshot@4.1.6)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.6)': + dependencies: + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + cjs-module-lexer: 1.2.3 + esbuild: 0.27.4 + miniflare: 4.20260603.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + wrangler: 4.98.0(@cloudflare/workers-types@4.20260605.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + zod: 3.25.76 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - bufferutil + - utf-8-validate + '@cloudflare/workerd-darwin-64@1.20260603.1': optional: true @@ -27722,7 +27749,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) '@vitest/expect@3.2.4': dependencies: @@ -27800,7 +27827,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) '@vitest/utils@3.2.4': dependencies: @@ -30545,6 +30572,11 @@ snapshots: expo-dev-menu-interface: 57.0.0(expo@57.0.8) react-native: 0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo-device@57.0.1(expo@57.0.8): + dependencies: + expo: 57.0.8(@babel/core@7.29.0)(@expo/metro-runtime@57.0.7)(bufferutil@4.1.0)(expo-router@57.0.8)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.0)(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + ua-parser-js: 0.7.41 + expo-document-picker@57.0.1(expo@57.0.8): dependencies: expo: 57.0.8(@babel/core@7.29.0)(@expo/metro-runtime@57.0.7)(bufferutil@4.1.0)(expo-router@57.0.8)(react-dom@19.2.6(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.0)(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) @@ -35353,12 +35385,13 @@ snapshots: dependencies: '@posthog/core': 1.4.0 - posthog-react-native@4.59.0(expo-application@57.0.2(expo@57.0.8))(expo-file-system@57.0.1(expo@57.0.8)(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo-localization@57.0.1(expo@57.0.8)(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)): + posthog-react-native@4.59.0(expo-application@57.0.2(expo@57.0.8))(expo-device@57.0.1(expo@57.0.8))(expo-file-system@57.0.1(expo@57.0.8)(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo-localization@57.0.1(expo@57.0.8)(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)): dependencies: '@posthog/core': 1.45.0 '@posthog/types': 1.398.0 optionalDependencies: expo-application: 57.0.2(expo@57.0.8) + expo-device: 57.0.1(expo@57.0.8) expo-file-system: 57.0.1(expo@57.0.8)(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-localization: 57.0.1(expo@57.0.8)(react@19.2.3) react-native-safe-area-context: 5.7.0(react-native@0.86.0(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -37626,6 +37659,8 @@ snapshots: typescript@6.0.3: {} + ua-parser-js@0.7.41: {} + uc.micro@2.1.0: {} ufo@1.6.4: {}