diff --git a/.changeset/core-flag-called-react-native-version.md b/.changeset/core-flag-called-react-native-version.md new file mode 100644 index 0000000000..2e6897b160 --- /dev/null +++ b/.changeset/core-flag-called-react-native-version.md @@ -0,0 +1,5 @@ +--- +'@posthog/core': patch +--- + +Keep `$react_native_version` on minimal `$feature_flag_called` events diff --git a/.changeset/react-native-version-property.md b/.changeset/react-native-version-property.md new file mode 100644 index 0000000000..4354e05674 --- /dev/null +++ b/.changeset/react-native-version-property.md @@ -0,0 +1,5 @@ +--- +'posthog-react-native': minor +--- + +Add `$react_native_version` (for example `0.79.6`) to events captured from JavaScript diff --git a/packages/core/src/featureFlagUtils.ts b/packages/core/src/featureFlagUtils.ts index ef0f4bf5eb..2e3fa33f92 100644 --- a/packages/core/src/featureFlagUtils.ts +++ b/packages/core/src/featureFlagUtils.ts @@ -252,6 +252,8 @@ export const MINIMAL_FLAG_CALLED_EVENT_PROPERTIES: readonly string[] = [ '$lib_version', '$device_id', '$is_server', + // Runtime identity, kept for runtime breakdowns of flag calls + '$react_native_version', ] /** diff --git a/packages/react-native/references/posthog-react-native-references-latest.json b/packages/react-native/references/posthog-react-native-references-latest.json index 24bb425336..8ec178bd13 100644 --- a/packages/react-native/references/posthog-react-native-references-latest.json +++ b/packages/react-native/references/posthog-react-native-references-latest.json @@ -3347,6 +3347,11 @@ "type": "string | null", "name": "$os_version" }, + { + "description": "React Native version like \"0.79.6\"", + "type": "string | null", + "name": "$react_native_version" + }, { "description": "Timezone of the device like \"Europe/Berlin\"", "type": "string | null", diff --git a/packages/react-native/src/native-deps.tsx b/packages/react-native/src/native-deps.tsx index 8cc1b4af34..67e295837c 100644 --- a/packages/react-native/src/native-deps.tsx +++ b/packages/react-native/src/native-deps.tsx @@ -1,5 +1,5 @@ import { OptionalAsyncStorage } from './optional/OptionalAsyncStorage' -import { GLOBAL_OBJ, getPlatformOS, isMacOS, isWeb, isWindows } from './utils' +import { GLOBAL_OBJ, getPlatformOS, getReactNativeVersion, isMacOS, isWeb, isWindows } from './utils' import { OptionalExpoApplication } from './optional/OptionalExpoApplication' import { OptionalExpoDevice } from './optional/OptionalExpoDevice' import { OptionalExpoFileSystem } from './optional/OptionalExpoFileSystem' @@ -39,6 +39,11 @@ export const getAppProperties = (): PostHogCustomAppProperties => { $device_type: currentDeviceType, } + const reactNativeVersion = getReactNativeVersion() + if (reactNativeVersion) { + properties.$react_native_version = reactNativeVersion + } + if (OptionalExpoApplication) { properties.$app_build = OptionalExpoApplication.nativeBuildVersion properties.$app_name = OptionalExpoApplication.applicationName diff --git a/packages/react-native/src/types.ts b/packages/react-native/src/types.ts index cc22cdcfe2..4029443153 100644 --- a/packages/react-native/src/types.ts +++ b/packages/react-native/src/types.ts @@ -154,6 +154,8 @@ export interface PostHogCustomAppProperties { $timezone?: string | null /** Whether the app is running on an emulator/simulator */ $is_emulator?: boolean | null + /** React Native version like "0.79.6" */ + $react_native_version?: string | null } export type PostHogSessionReplayConfig = { diff --git a/packages/react-native/src/utils.ts b/packages/react-native/src/utils.ts index acd597046f..91e071f107 100644 --- a/packages/react-native/src/utils.ts +++ b/packages/react-native/src/utils.ts @@ -51,3 +51,20 @@ export function isWindows(): boolean { } export const isHermes = () => !!GLOBAL_OBJ.HermesInternal + +type ReactNativeVersion = { major: number; minor: number; patch: number; prerelease?: number | string | null } + +/** React Native version like "0.79.6" or "0.80.0-rc.2", or undefined where Platform.constants doesn't report it (e.g. web) */ +export function getReactNativeVersion(): string | undefined { + try { + const version = (Platform as { constants?: { reactNativeVersion?: ReactNativeVersion } }).constants + ?.reactNativeVersion + if (!version || typeof version.major !== 'number') { + return undefined + } + const { major, minor, patch, prerelease } = version + return `${major}.${minor}.${patch}${prerelease != null ? `-${prerelease}` : ''}` + } catch { + return undefined + } +} diff --git a/packages/react-native/test/react-native-version.spec.ts b/packages/react-native/test/react-native-version.spec.ts new file mode 100644 index 0000000000..756be09e8f --- /dev/null +++ b/packages/react-native/test/react-native-version.spec.ts @@ -0,0 +1,67 @@ +import { Platform } from 'react-native' +import { minimizeFlagCalledEventProperties } from '@posthog/core' +import { PostHog } from '../src' +import { getReactNativeVersion } from '../src/utils' + +const platform = Platform as typeof Platform & { constants?: unknown } + +describe('$react_native_version', () => { + afterEach(() => { + delete platform.constants + }) + + it('formats Platform.constants.reactNativeVersion', () => { + platform.constants = { reactNativeVersion: { major: 0, minor: 79, patch: 6, prerelease: null } } + expect(getReactNativeVersion()).toBe('0.79.6') + }) + + it('appends the prerelease tag', () => { + platform.constants = { reactNativeVersion: { major: 0, minor: 80, patch: 0, prerelease: 'rc.2' } } + expect(getReactNativeVersion()).toBe('0.80.0-rc.2') + }) + + it('is undefined when the platform does not report a version', () => { + platform.constants = {} + expect(getReactNativeVersion()).toBeUndefined() + }) + + it('is undefined when the version object has no numeric fields', () => { + platform.constants = { reactNativeVersion: {} } + expect(getReactNativeVersion()).toBeUndefined() + }) + + it('is undefined when reading the constants throws', () => { + Object.defineProperty(platform, 'constants', { + configurable: true, + get: () => { + throw new Error('getConstants failed') + }, + }) + expect(getReactNativeVersion()).toBeUndefined() + }) + + it('is sent with every event', async () => { + platform.constants = { reactNativeVersion: { major: 0, minor: 79, patch: 6 } } + const posthog = new PostHog('test-token', { flushInterval: 0 }) + await posthog.ready() + + expect(posthog.getCommonEventProperties().$react_native_version).toBe('0.79.6') + + await posthog.shutdown() + }) + + it('survives minimal $feature_flag_called events', () => { + expect(minimizeFlagCalledEventProperties({ $react_native_version: '0.79.6', custom: 'dropped' })).toEqual({ + $react_native_version: '0.79.6', + }) + }) + + it('is omitted when the platform does not report a version', async () => { + const posthog = new PostHog('test-token', { flushInterval: 0 }) + await posthog.ready() + + expect(posthog.getCommonEventProperties()).not.toHaveProperty('$react_native_version') + + await posthog.shutdown() + }) +})