Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/react-native-release-coordinates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-react-native': patch
---

Key the release an iOS build uploads on the app's Info.plist rather than on Xcode's `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION`. The SDK reports `$app_version` and `$app_build` from Info.plist, and Expo writes literal versions there while leaving the build settings at the Xcode template default of `1.0`. A build whose app reported `1.0.0` therefore created its release as `1.0`, so the release did not describe the app that shipped. Releases created by an Expo build now carry the app's real version, which means a project on the default release mode will start creating release rows under that version instead. A project whose Info.plist references the build settings, as a bare React Native app does, is unaffected.
117 changes: 116 additions & 1 deletion packages/react-native/test/posthog-xcode-parse.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execFileSync, execSync } from 'child_process'
import { execFileSync, execSync, spawnSync } from 'child_process'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
Expand Down Expand Up @@ -217,3 +217,118 @@ print_command_error "posthog-cli hermes upload" "42" "$CLI_OUTPUT"`
expect(output.every((line) => line.startsWith('error: '))).toBe(true)
})
})

describe('posthog-xcode.sh posthog-cli invocation', () => {
// Runs the wrapper against a posthog-cli stub that records its arguments, so the assertions
// are on what the CLI was actually asked to do rather than on the shell source.
const runWrapper = (
args: string[],
extraEnv: Record<string, string>,
infoPlist?: Record<string, string>
): { status: number; invocations: string[]; output: string } => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'posthog-xcode-release-mode-'))
try {
const derivedDir = path.join(tempDir, 'derived')
const configurationDir = path.join(tempDir, 'configuration')
const homeDir = path.join(tempDir, 'home')
const iosDir = path.join(tempDir, 'ios')
const cliTracePath = path.join(tempDir, 'cli.log')
const cliPath = path.join(homeDir, '.posthog', 'posthog-cli')
const reactNativePath = path.join(tempDir, 'react-native-xcode.sh')

for (const directory of [derivedDir, configurationDir, iosDir, path.dirname(cliPath)]) {
fs.mkdirSync(directory, { recursive: true })
}
fs.writeFileSync(cliPath, '#!/bin/sh\necho "$@" >> "$CLI_TRACE_PATH"\n', { mode: 0o755 })
fs.writeFileSync(reactNativePath, '#!/bin/sh\nexit 0\n', { mode: 0o755 })

const plistEnv: Record<string, string> = {}
if (infoPlist) {
const entries = Object.entries(infoPlist)
.map(([key, value]) => ` <key>${key}</key>\n <string>${value}</string>`)
.join('\n')
fs.mkdirSync(path.join(iosDir, 'App'), { recursive: true })
fs.writeFileSync(
path.join(iosDir, 'App', 'Info.plist'),
`<?xml version="1.0" encoding="UTF-8"?>\n<plist version="1.0">\n<dict>\n${entries}\n</dict>\n</plist>\n`
)
plistEnv.SRCROOT = iosDir
plistEnv.INFOPLIST_FILE = 'App/Info.plist'
}

const result = spawnSync(SCRIPT_PATH, [...args, '/bin/sh', reactNativePath], {
cwd: iosDir,
env: {
...process.env,
CLI_TRACE_PATH: cliTracePath,
CONFIGURATION_BUILD_DIR: configurationDir,
DERIVED_FILE_DIR: derivedDir,
// Stands in for a CI runner so the wrapper skips deriving git metadata from the
// (repo-less) temp directory.
GITHUB_SHA: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
HOME: homeDir,
NODE_BINARY: process.execPath,
...plistEnv,
...extraEnv,
},
encoding: 'utf8',
})

const invocations = fs.existsSync(cliTracePath)
? fs.readFileSync(cliTracePath, 'utf8').trim().split('\n').filter(Boolean)
: []
return { status: result.status ?? -1, invocations, output: `${result.stdout}${result.stderr}` }
} finally {
fs.rmSync(tempDir, { recursive: true, force: true })
}
}

// The SDK reports $app_version and $app_build from Info.plist, and event release mode resolves
// an exception's release from exactly those. Expo writes literal versions there and leaves
// MARKETING_VERSION at the Xcode template default of 1.0, so a release keyed on the build
// setting never matches an event and the exception silently reports no release.
it('keys the release on Info.plist rather than the build settings', () => {
const { status, invocations } = runWrapper(
[],
{
PRODUCT_BUNDLE_IDENTIFIER: 'com.example.app',
MARKETING_VERSION: '1.0',
CURRENT_PROJECT_VERSION: '1',
},
{ CFBundleShortVersionString: '1.0.0', CFBundleVersion: '42' }
)

expect(status).toBe(0)
expect(invocations[1]).toContain('--release-name com.example.app')
expect(invocations[1]).toContain('--release-version 1.0.0')
expect(invocations[1]).toContain('--build 42')
})

it('falls back to the build settings when Info.plist only references them', () => {
const { status, invocations } = runWrapper(
[],
{
PRODUCT_BUNDLE_IDENTIFIER: 'com.example.app',
MARKETING_VERSION: '2.5.0',
CURRENT_PROJECT_VERSION: '7',
},
{ CFBundleShortVersionString: '$(MARKETING_VERSION)', CFBundleVersion: '$(CURRENT_PROJECT_VERSION)' }
)

expect(status).toBe(0)
expect(invocations[1]).toContain('--release-version 2.5.0')
expect(invocations[1]).toContain('--build 7')
})

it('falls back to the build settings when there is no Info.plist at all', () => {
const { status, invocations } = runWrapper([], {
PRODUCT_BUNDLE_IDENTIFIER: 'com.example.app',
MARKETING_VERSION: '3.1.4',
CURRENT_PROJECT_VERSION: '9',
})

expect(status).toBe(0)
expect(invocations[1]).toContain('--release-version 3.1.4')
expect(invocations[1]).toContain('--build 9')
})
})
40 changes: 35 additions & 5 deletions packages/react-native/tooling/posthog-xcode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,46 @@ fi
# mimics how the file is defined in node_modules/react-native/scripts/react-native-xcode.sh (PACKAGER_SOURCEMAP_FILE)
SOURCEMAP_PACKAGER_FILE="$CONFIGURATION_BUILD_DIR/$SOURCEMAP_NAME"

# Pass release info from Xcode build settings when available
# Read a literal value out of the target's Info.plist. Returns nothing when the key is absent or
# still holds an unexpanded build-setting reference such as $(MARKETING_VERSION), which tells the
# caller to use the build setting instead.
posthog_plist_value() {
posthog_plist_file="${SRCROOT:-}/${INFOPLIST_FILE:-}"
if [ -z "${INFOPLIST_FILE:-}" ] || [ ! -f "$posthog_plist_file" ]; then
return 0
fi
posthog_plist_result=$(/usr/libexec/PlistBuddy -c "Print :$1" "$posthog_plist_file" 2>/dev/null) || return 0
case "$posthog_plist_result" in
*'$('*) return 0 ;;
esac
Comment on lines +139 to +141

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Custom plist substitutions are discarded

When CFBundleShortVersionString or CFBundleVersion references a custom or compound Xcode setting such as $(APP_VERSION), this branch discards it and falls back specifically to MARKETING_VERSION or CURRENT_PROJECT_VERSION, causing the uploaded release coordinates to differ from the processed values reported by the runtime SDK.

Knowledge Base Used: React Native SDK

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-native/tooling/posthog-xcode.sh
Line: 139-141

Comment:
**Custom plist substitutions are discarded**

When `CFBundleShortVersionString` or `CFBundleVersion` references a custom or compound Xcode setting such as `$(APP_VERSION)`, this branch discards it and falls back specifically to `MARKETING_VERSION` or `CURRENT_PROJECT_VERSION`, causing the uploaded release coordinates to differ from the processed values reported by the runtime SDK.

**Knowledge Base Used:** [React Native SDK](https://app.greptile.com/posthog-org-19734/-/custom-context/knowledge-base/posthog/posthog-js/-/docs/react-native-sdk.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

printf '%s' "$posthog_plist_result"
}

# The SDK reports $app_version and $app_build from the app's Info.plist. Xcode's MARKETING_VERSION
# and CURRENT_PROJECT_VERSION are only the usual source for those keys. Expo writes literal
# versions into Info.plist and leaves the build settings at their template defaults, so the two
# disagree, and a release keyed on the build setting does not describe the app that ships. Read the
# plist that ships, and fall back to the build setting when it holds a reference to one.
POSTHOG_APP_VERSION=$(posthog_plist_value CFBundleShortVersionString)
if [ -z "$POSTHOG_APP_VERSION" ]; then
POSTHOG_APP_VERSION="${MARKETING_VERSION:-}"
fi
POSTHOG_APP_BUILD=$(posthog_plist_value CFBundleVersion)
if [ -z "$POSTHOG_APP_BUILD" ]; then
POSTHOG_APP_BUILD="${CURRENT_PROJECT_VERSION:-}"
fi

# The bundle identifier is read from the build setting alone. Info.plist normally references it
# rather than repeating it, and the SDK reports the resolved value.
CLI_RELEASE_ARGS=""
if [ -n "${PRODUCT_BUNDLE_IDENTIFIER}" ]; then
CLI_RELEASE_ARGS="$CLI_RELEASE_ARGS --release-name $PRODUCT_BUNDLE_IDENTIFIER"
fi
if [ -n "${MARKETING_VERSION}" ]; then
CLI_RELEASE_ARGS="$CLI_RELEASE_ARGS --release-version $MARKETING_VERSION"
if [ -n "$POSTHOG_APP_VERSION" ]; then
CLI_RELEASE_ARGS="$CLI_RELEASE_ARGS --release-version $POSTHOG_APP_VERSION"
fi
if [ -n "${CURRENT_PROJECT_VERSION}" ]; then
CLI_RELEASE_ARGS="$CLI_RELEASE_ARGS --build $CURRENT_PROJECT_VERSION"
if [ -n "$POSTHOG_APP_BUILD" ]; then
CLI_RELEASE_ARGS="$CLI_RELEASE_ARGS --build $POSTHOG_APP_BUILD"
fi

# RN deletes the PACKAGER_SOURCEMAP_FILE file after execution but we need it
Expand Down
Loading