Skip to content

feat(appsflyer): add startAppsFlyer to safely resume a withheld start() - #1064

Merged
nickolas-dimitrakas merged 1 commit into
mainfrom
fix/appsflyer-manual-start-wrapper
Oct 2, 2026
Merged

nickolas-dimitrakas merged 1 commit into
mainfrom
fix/appsflyer-manual-start-wrapper

Conversation

@nickolas-dimitrakas

@nickolas-dimitrakas nickolas-dimitrakas commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Why

Apps that enable the AppsFlyer kit's "manual start" setting rely on it to delay AppsFlyer's attribution until the right moment — for example, until the app's own consent screen is accepted. The kit already withholds the automatic start correctly, but it has no safe way to tell it to go afterward: a host app has to reach into AppsFlyer's own shared instance directly, and if that app also happens to link the AppsFlyer SDK anywhere else, that call can land on a second, unconfigured copy and fail outright. After this change, there is a method on the kit itself that always resumes the one instance it configured, so that failure mode cannot happen through this call, and it's documented so developers actually find it.

Programme

Standalone change, no programme.

What changes

Before: once manualStart withheld the automatic start, the only way to resume it was calling AppsFlyer's own shared-instance accessor directly from host app code, and nothing documented this setting at all.

After:

  • MPKitAppsFlyer exposes a new class method, startAppsFlyer, that starts the kit's own already-configured instance. It returns NO (and logs a warning) if called before mParticle has configured the kit, or before that configuration has reached the point of setting a dev key (see Risks).
  • The kit's own automatic-start call site, in didBecomeActive, now calls startAppsFlyer too instead of starting the tracker directly, so there is one code path for "start AppsFlyer" rather than two. Verified this doesn't change behavior: didBecomeActive only ever runs after the dev key is already set (traced both the self-triggered path in didFinishLaunchingWithConfiguration: and the externally-triggered path in MPKitContainerExecutionAdapter, which gates on the kit's started flag — set only after the dev key is assigned).
  • The kit's README now documents the setting and the method, with Swift and Objective-C samples.

A reviewer should start at startAppsFlyer in MPKitAppsFlyer.m (right after the existing setDelegate:) and its header doc in MPKitAppsFlyer.h; the NS_SWIFT_NAME annotation there is load-bearing, not decorative — see Risks.

Linked work

None.

Rollout

Path: ships as part of the next published version of the mParticle-AppsFlyer-6 kit (CocoaPods) / mparticle-apple-integration-appsflyer-6 (SPM); a consuming app only gets the new method, and the (behaviorally unchanged) internal rewiring, once it upgrades that dependency. No server-side or account-level component.
Feature flags: none.
Turning it off: revert this pull request and cut a new kit release. A consuming app that has started calling the new method would need to switch back to calling AppsFlyer directly, same as before this shipped.
What we watch: nothing in production changes for an app that doesn't adopt the new method, since the internal rewiring was verified behavior-preserving; if that verification is wrong, the symptom would be AppsFlyer not auto-starting for apps that never touched manualStart, which would surface immediately in this kit's own attribution.

Risks

  • Swift auto-renames a zero-argument +startAppsFlyer on a class named ...AppsFlyer down to a bare start(), confirmed by an actual compile failure while building this change, which would have collided with an unrelated existing method and produced a confusing public name. Contained by the NS_SWIFT_NAME(startAppsFlyer()) annotation added alongside it, verified by a passing Swift build against both Objective-C and Swift call sites. How we'd see it: Swift consumers of this kit would otherwise see MPKitAppsFlyer.start() instead of the intended MPKitAppsFlyer.startAppsFlyer() in autocomplete — a reviewer comparing this PR's header diff against a local Swift build would catch it, same as happened here.
  • startAppsFlyer's guard originally only checked for a nil tracker. CodeRabbit flagged that appsFlyerTracker is assigned before the rest of its configuration (dev key, delegates) runs, so a caller landing in that window would pass a nil check alone. Narrowed by also requiring a non-empty dev key, with a dedicated test (test_startAppsFlyer_withTrackerAssignedButNotYetConfigured_returnsFalse) using an unconfigured mock. This narrows the window rather than closing it outright — a caller landing between the dev key assignment and the delegate assignments a few lines later would still pass. Not fully addressed because closing it completely would mean adding kit-owned state solely to guard against a caller racing the kit's own synchronous initialization, which every production call path (traced for both didBecomeActive paths) cannot actually do. How we'd see it: a consuming app would see attribution or deep-link callbacks silently not fire for a session that hit the window; given the narrow trigger (another thread calling this during the single synchronous method that configures the tracker), this hasn't been reported before and isn't expected now.
  • The didBecomeActive rewiring is additive in effect, not just in code — confirmed no production call path reaches it before the dev key is set. Contained by two independent traces: the self-triggered path (didFinishLaunchingWithConfiguration: sets the dev key, then flips the kit's started flag, then dispatches the call to didBecomeActive) and the externally-triggered path (MPKitContainerExecutionAdapter only calls didBecomeActive on kits whose started flag is already set). How we'd see it: same as above — a regression here would show as AppsFlyer failing to auto-start for apps not using manualStart at all, which would be immediately visible.
  • Risk class: low.

Who

Written by: an automated coding agent (Claude Code), prompted by an internal defect report; no public link available.
Code reviewed before opening: an automated adversarial reviewer, across two passes — once before this PR was first opened (found one documentation typo, fixed), and again after extending it with the dev-key guard, the didBecomeActive rewiring, and the README section (found no blocking issues; confirmed the "no behavior change" claim against the actual call graph rather than accepting it as asserted).
Design reviewed before opening: no one.
Decision this implements: give the current AppsFlyer kit the same safe manual-start resume path an older, no-longer-maintained version of this kit once had, and address CodeRabbit's review comment on this PR; no separate design record.
Checked: xcodebuild test -scheme mParticle-AppsFlyer -destination "platform=iOS Simulator,id=<booted sim>" against the kit's SwiftPM package (46 tests, all passing) and trunk check on all four changed files, today.
Not checked: no instrumented run against a real AppsFlyer dev key / physical device; no verification against the CocoaPods-static-library linking scenario startAppsFlyer exists to prevent (reproducing a duplicate-instance link error reliably needs a throwaway host app and isn't practical to do here).

Size

Hand-written: 76 lines across 4 files.
Generated: none.

Notes for reviewers

Files changed:

  • Kits/appsflyer/appsflyer-6/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m — the new startAppsFlyer method (with the dev-key guard), and didBecomeActive now calling it
  • Kits/appsflyer/appsflyer-6/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h — its public declaration, header doc, and the NS_SWIFT_NAME annotation
  • Kits/appsflyer/appsflyer-6/Tests/mParticle-AppsFlyerTests/MPKitAppsFlyerTests.swift — three new tests; setUp() now gives the shared mock a dev key so existing tests still reflect a "configured" tracker
  • Kits/appsflyer/appsflyer-6/README.md — new "Manual Start" section with Swift and Objective-C samples

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🐦 Swift Migration Progress

Production implementation code at ffa5526d21f5 compared with 22ef90576b01.

Area Goal Progress Base This PR Swift SLOC Objective-C remaining Change
Core SDK Short term — in scope ████████░░ 80.54% 80.54% 13,426 3,244 ➖ 0.00 pp
Core SDK Long term — all Objective-C █████▉░░░░ 58.18% 58.18% 13,426 9,649 ➖ 0.00 pp
SDK kit infrastructure Short term — in scope ████████▉░ 88.49% 88.49% 2,244 292 ➖ 0.00 pp
SDK kit infrastructure Long term — all Objective-C ████▌░░░░░ 45.00% 45.00% 2,244 2,743 ➖ 0.00 pp
Standalone kits Short term — in scope ▋░░░░░░░░░ 5.87% 5.86% 897 14,401 ↩️ -0.01 pp
Standalone kits Long term — all Objective-C ▋░░░░░░░░░ 5.87% 5.86% 897 14,401 ↩️ -0.01 pp

Objective-C retained by design: Core SDK 6,405 · SDK kit infrastructure 2,451 · Standalone kits 0.

This PR's code movement

Area Swift lines added Objective-C lines removed
Core SDK 0 0
SDK kit infrastructure 0 0
Standalone kits 0 1
How this is measured
  • Current composition uses production source lines of code (SLOC) from cloc; comments and blank lines are excluded.
  • Short term — in scope excludes the Objective-C the migration will not delete, so 100% completes the in-scope conversions: every in-scope implementation gone. Migration continues on main after the integration branch merge.
  • Long term — all Objective-C keeps the full denominator. Reaching 100% there means the public API itself becomes Swift, which is a breaking change reserved for a future major release.
  • The gap between the two rows is the retained public/kit contract, runtime-identity, and boundary-glue surface listed in Tools/swift-migration-retained-objc.txt.
  • Retained wrappers keep their Objective-C interface but still shed logic to Swift. That thinning moves the long-term row and the retained figure, not the short-term row.
  • Both revisions are measured with the manifest from the head revision, so a manifest edit does not by itself move the reported change. A retained file this pull request renamed or deleted still counts as retained at the base.
  • Pull request movement uses physical additions/deletions from git diff base...head --numstat; it counts retained files too and is intentionally separate from SLOC totals.
  • Core excludes SDK kit infrastructure and vendored libraries. Standalone kits include only files below Kits/**/Sources.
  • Tests, examples, headers, build outputs, vendored libraries, and the MParticle/Sources Swift overlay are excluded.
  • Objective-C++ (.mm) is included in the Objective-C figures and removed counts.

Generated with cloc 2.10. This report is informational and does not gate migration direction.

@nickolas-dimitrakas nickolas-dimitrakas self-assigned this Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

📦 SDK Size Impact Report

Measures how much the SDK adds to an app's size (with-SDK minus without-SDK).

Metric Target Branch This PR Change
App Bundle Impact 2.70 MB 2.71 MB +4 KB
Executable Impact 848 bytes 848 bytes +N/A
Framework binary (ships) 2.98 MB 2.99 MB +4 KB

➡️ SDK size impact change is minimal.

Where the bytes are

Component Target branch This PR Change
Executable code (__text) 1191.7 KB 1190.5 KB -1.2 KB
Swift metadata (__swift5_*) 55.4 KB 55.4 KB -0.0 KB
ObjC metadata (__objc_*) 401.9 KB 401.9 KB +0.0 KB
Symbol tables (__LINKEDIT) 816.0 KB 816.0 KB +0.0 KB
Mach-O images 2 2 +0
Exported symbols 6026 6026 +0
Debug symbols (not shipped to users)

The SDK is embedded as a dynamic framework, so the app's own executable barely
moves and App Bundle Impact is the number that tracks shipped code.
xcodebuild -create-xcframework folds the dSYM in beside the framework, so the
xcframework total is mostly debug symbols and is not a shipping cost.

Target Branch This PR
dSYMs 3.73 MB 3.73 MB
XCFramework total 6.72 MB 6.72 MB
Raw measurements

Target branch (main):

{"baseline_app_size_kb":84,"baseline_executable_size_bytes":75464,"with_sdk_app_size_kb":2852,"with_sdk_executable_size_bytes":76312,"sdk_impact_kb":2768,"sdk_executable_impact_bytes":848,"xcframework_size_kb":6880,"framework_size_kb":3056,"dsym_size_kb":3820}

This PR:

{"baseline_app_size_kb":84,"baseline_executable_size_bytes":75464,"with_sdk_app_size_kb":2856,"with_sdk_executable_size_bytes":76312,"sdk_impact_kb":2772,"sdk_executable_impact_bytes":848,"xcframework_size_kb":6884,"framework_size_kb":3060,"dsym_size_kb":3820}

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: QUIET

Plan: Enterprise

Run ID: 87493431-9588-4b9f-a342-a297d0b493a4

📥 Commits

Reviewing files that changed from the base of the PR and between 4480524 and ffa5526.

📒 Files selected for processing (3)
  • Kits/appsflyer/appsflyer-6/README.md
  • Kits/appsflyer/appsflyer-6/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m
  • Kits/appsflyer/appsflyer-6/Tests/mParticle-AppsFlyerTests/MPKitAppsFlyerTests.swift

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.


📝 Walkthrough

Walkthrough

MPKitAppsFlyer adds the public class method startAppsFlyer. It logs a warning and returns NO when the tracker is absent or its developer key is empty. Otherwise, it starts the tracker and returns YES. When manual start is disabled, didBecomeActive calls this method. Tests cover configured and unconfigured trackers. The README documents how to resume AppsFlyer when manual start is enabled.

Priority: ⬇️ Low

Merge Risk: 🟡 Moderate · up to ffa55

A concurrent host call during initialization can start AppsFlyer before the kit finishes configuring it. Guard startup until configuration is complete before merging.

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: QUIET

Plan: Enterprise

Run ID: be18610e-3c0e-46e7-88c2-b2447b345f91

📥 Commits

Reviewing files that changed from the base of the PR and between 22ef905 and 4480524.

📒 Files selected for processing (3)
  • Kits/appsflyer/appsflyer-6/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m
  • Kits/appsflyer/appsflyer-6/Sources/mParticle-AppsFlyer/include/MPKitAppsFlyer.h
  • Kits/appsflyer/appsflyer-6/Tests/mParticle-AppsFlyerTests/MPKitAppsFlyerTests.swift

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

Comment thread Kits/appsflyer/appsflyer-6/Sources/mParticle-AppsFlyer/MPKitAppsFlyer.m Outdated
@nickolas-dimitrakas
nickolas-dimitrakas marked this pull request as ready for review October 2, 2026 18:16
@nickolas-dimitrakas
nickolas-dimitrakas requested a review from a team as a code owner October 2, 2026 18:16
@cursor

cursor Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

PR Summary

Low Risk
Low risk, additive public method with unit test coverage and no impact on existing automatic start flows.

Overview
Adds a public +startAppsFlyer class method to MPKitAppsFlyer (exposed to Swift as startAppsFlyer()) to safely resume tracking on the kit-configured AppsFlyer instance when manual start is enabled.

Internal activation logic in didBecomeActive now delegates to this new method, accompanied by documentation in the README and unit tests covering initialized, uninitialized, and unconfigured tracker states.

Reviewed by Cursor Bugbot for commit ffa5526. Bugbot is set up for automated code reviews on this repo. Configure here.

The manualStart setting already withholds AppsFlyerLib's automatic start()
call correctly, but gave host apps no safe way to resume it afterward other
than calling AppsFlyerLib.shared().start() directly, which can resolve a
different, unconfigured instance if the app also links the AppsFlyer SDK
elsewhere. Adds a public +[MPKitAppsFlyer startAppsFlyer] that always
operates on the kit's own configured instance instead, and routes the kit's
own automatic-start call through it too so there is one code path. Guards
against starting a tracker that's been assigned but not yet configured
(dev key not set), and documents the method in the kit's README.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nickolas-dimitrakas
nickolas-dimitrakas force-pushed the fix/appsflyer-manual-start-wrapper branch from 4480524 to ffa5526 Compare October 2, 2026 18:26
@nickolas-dimitrakas
nickolas-dimitrakas merged commit 801085a into main Oct 2, 2026
84 checks passed
@nickolas-dimitrakas
nickolas-dimitrakas deleted the fix/appsflyer-manual-start-wrapper branch October 2, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants