chore(ios): add native app launch lifecycle trace diagnostics (temporary)

Add temporary, append-only tracing of the native iOS application launch
lifecycle to diagnose failed cold-start shares when Xcode is not
attached during launch. Writes app-launch-trace.log in the same App
Group container as share-extension-trace.log, reusing the same style
(ISO8601 timestamps, append-only, all logging failures swallowed).

AppDelegate now traces every lifecycle callback
(didFinishLaunchingWithOptions, application(open:),
applicationDidBecomeActive, applicationWill/DidEnterForeground/Background,
applicationWillResignActive, applicationWillTerminate, and
checkForSharedImageOnActivation), recording the callback name, whether a
URL was supplied, the URL value, whether it matches timesafari://,
whether launch options carried a URL, and whether shared-image
activation ran.

Expose read-only getAppLaunchTrace()/clearAppLaunchTrace() plugin APIs
(mirroring the share-extension trace APIs) with matching TypeScript
definitions and web stubs. main.capacitor.ts logs the full launch trace
between APP LAUNCH TRACE START/END markers inside the existing
diagnostics block.

No share-target behavior changed; Android untouched. All additions are
marked TEMPORARY SHARE TARGET DIAGNOSTICS.
This commit is contained in:
Jose Olarte III
2026-06-26 17:06:50 +08:00
parent c1a5bae5c8
commit 256018d30d
6 changed files with 157 additions and 2 deletions

View File

@@ -9,6 +9,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// TEMPORARY SHARE TARGET DIAGNOSTICS
let launchURL = launchOptions?[.url] as? URL
// TEMPORARY SHARE TARGET DIAGNOSTICS
SharedImageUtility.appendAppLaunchTrace(
"didFinishLaunchingWithOptions urlSupplied=\(launchURL != nil) url=\(launchURL?.absoluteString ?? "nil") matchesTimesafari=\(AppDelegate.isTimesafariURL(launchURL)) launchOptionsHasURL=\(launchURL != nil) sharedImageActivationInvoked=false"
)
// Set notification center delegate so notifications show in foreground and rollover is triggered
UNUserNotificationCenter.current().delegate = self
@@ -61,20 +68,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func applicationWillResignActive(_ application: UIApplication) {
// TEMPORARY SHARE TARGET DIAGNOSTICS
SharedImageUtility.appendAppLaunchTrace(
"applicationWillResignActive urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=false"
)
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// TEMPORARY SHARE TARGET DIAGNOSTICS
SharedImageUtility.appendAppLaunchTrace(
"applicationDidEnterBackground urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=false"
)
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// TEMPORARY SHARE TARGET DIAGNOSTICS
SharedImageUtility.appendAppLaunchTrace(
"applicationWillEnterForeground urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=false"
)
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// TEMPORARY SHARE TARGET DIAGNOSTICS
SharedImageUtility.appendAppLaunchTrace(
"applicationDidBecomeActive urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=true"
)
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
// Re-set notification delegate when app becomes active (in case Capacitor resets it)
@@ -116,7 +139,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
*/
private func checkForSharedImageOnActivation() {
// Check if shared photo is ready
if SharedImageUtility.isSharedPhotoReady() {
// TEMPORARY SHARE TARGET DIAGNOSTICS
let isReady = SharedImageUtility.isSharedPhotoReady()
// TEMPORARY SHARE TARGET DIAGNOSTICS
SharedImageUtility.appendAppLaunchTrace(
"checkForSharedImageOnActivation urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=true sharedPhotoReady=\(isReady)"
)
if isReady {
// Clear the flag
SharedImageUtility.clearSharedPhotoReadyFlag()
@@ -127,10 +156,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func applicationWillTerminate(_ application: UIApplication) {
// TEMPORARY SHARE TARGET DIAGNOSTICS
SharedImageUtility.appendAppLaunchTrace(
"applicationWillTerminate urlSupplied=false url=nil matchesTimesafari=false launchOptionsHasURL=false sharedImageActivationInvoked=false"
)
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
// TEMPORARY SHARE TARGET DIAGNOSTICS
SharedImageUtility.appendAppLaunchTrace(
"application(open:) urlSupplied=true url=\(url.absoluteString) matchesTimesafari=\(AppDelegate.isTimesafariURL(url)) launchOptionsHasURL=false sharedImageActivationInvoked=false"
)
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
// Note: Share Extension opens app with timesafari:// (empty path), which is handled by JavaScript
@@ -138,6 +175,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
/// Returns true when the supplied URL uses the timesafari:// scheme.
/// Diagnostics-only helper; does not affect URL handling.
private static func isTimesafariURL(_ url: URL?) -> Bool {
return url?.scheme?.lowercased() == "timesafari"
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Called when the app was launched with an activity, including Universal Links.
// Feel free to add additional processing here, but if you want the App API to support

View File

@@ -29,7 +29,11 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin {
// TEMPORARY SHARE TARGET DIAGNOSTICS
CAPPluginMethod(#selector(getShareExtensionTrace(_:)), returnType: .promise),
// TEMPORARY SHARE TARGET DIAGNOSTICS
CAPPluginMethod(#selector(clearShareExtensionTrace(_:)), returnType: .promise)
CAPPluginMethod(#selector(clearShareExtensionTrace(_:)), returnType: .promise),
// TEMPORARY SHARE TARGET DIAGNOSTICS
CAPPluginMethod(#selector(getAppLaunchTrace(_:)), returnType: .promise),
// TEMPORARY SHARE TARGET DIAGNOSTICS
CAPPluginMethod(#selector(clearAppLaunchTrace(_:)), returnType: .promise)
]
}
@@ -93,5 +97,24 @@ public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin {
SharedImageUtility.clearShareExtensionTrace()
call.resolve()
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
/**
* Return the raw app launch lifecycle trace log from the App Group container
*/
@objc public func getAppLaunchTrace(_ call: CAPPluginCall) {
call.resolve([
"trace": SharedImageUtility.getAppLaunchTrace()
])
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
/**
* Delete the app launch lifecycle trace log if present
*/
@objc public func clearAppLaunchTrace(_ call: CAPPluginCall) {
SharedImageUtility.clearAppLaunchTrace()
call.resolve()
}
}

View File

@@ -18,6 +18,8 @@ public class SharedImageUtility {
private static let sharedPhotoReadyKey = "sharedPhotoReady"
// TEMPORARY SHARE TARGET DIAGNOSTICS
private static let shareExtensionTraceFileName = "share-extension-trace.log"
// TEMPORARY SHARE TARGET DIAGNOSTICS
private static let appLaunchTraceFileName = "app-launch-trace.log"
/// Get the App Group container URL for accessing shared files
private static var appGroupContainerURL: URL? {
@@ -174,6 +176,54 @@ public class SharedImageUtility {
try? FileManager.default.removeItem(at: fileURL)
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
/**
* Append a single timestamped line to the app launch trace file in the
* App Group container. Each line is prefixed with an ISO8601 timestamp.
* Append-only; logging failures are intentionally swallowed (diagnostics only).
*/
static func appendAppLaunchTrace(_ message: String) {
guard let containerURL = appGroupContainerURL else { return }
let fileURL = containerURL.appendingPathComponent(appLaunchTraceFileName)
let timestamp = ISO8601DateFormatter().string(from: Date())
let line = "\(timestamp) \(message)\n"
guard let data = line.data(using: .utf8) else { return }
if let handle = try? FileHandle(forWritingTo: fileURL) {
defer { try? handle.close() }
_ = try? handle.seekToEnd()
try? handle.write(contentsOf: data)
} else {
try? data.write(to: fileURL, options: .atomic)
}
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
/**
* Read the app launch trace file from the App Group container.
* Read-only: does not modify or delete the trace file.
*
* @returns the full trace contents, or an empty string if no trace exists
*/
static func getAppLaunchTrace() -> String {
guard let containerURL = appGroupContainerURL else {
return ""
}
let fileURL = containerURL.appendingPathComponent(appLaunchTraceFileName)
return (try? String(contentsOf: fileURL, encoding: .utf8)) ?? ""
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
/**
* Delete the app launch trace file from the App Group container if present.
*/
static func clearAppLaunchTrace() {
guard let containerURL = appGroupContainerURL else {
return
}
let fileURL = containerURL.appendingPathComponent(appLaunchTraceFileName)
try? FileManager.default.removeItem(at: fileURL)
}
/**
* Check if shared photo ready flag is set
* This flag is set by the Share Extension when image is ready

View File

@@ -417,6 +417,15 @@ async function checkForSharedImageAndNavigate() {
"[ShareTarget] TRACE FIRST 500\n" + traceResult.trace.slice(0, 500),
);
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
const launchTraceResult = await SharedImage.getAppLaunchTrace();
// TEMPORARY SHARE TARGET DIAGNOSTICS
console.info("[ShareTarget] APP LAUNCH TRACE START");
// TEMPORARY SHARE TARGET DIAGNOSTICS
console.info(launchTraceResult.trace);
// TEMPORARY SHARE TARGET DIAGNOSTICS
console.info("[ShareTarget] APP LAUNCH TRACE END");
}
logger.debug("[Main] 🔍 Checking for shared image on app activation");

View File

@@ -10,6 +10,8 @@ import type {
ShareExtensionDiagnostics,
// TEMPORARY SHARE TARGET DIAGNOSTICS
ShareExtensionTrace,
// TEMPORARY SHARE TARGET DIAGNOSTICS
AppLaunchTrace,
} from "./definitions";
export class SharedImagePluginWeb
@@ -45,4 +47,14 @@ export class SharedImagePluginWeb
async clearShareExtensionTrace(): Promise<void> {
// Web platform doesn't support native sharing - no-op
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
async getAppLaunchTrace(): Promise<AppLaunchTrace> {
return { trace: "" };
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
async clearAppLaunchTrace(): Promise<void> {
// Web platform doesn't support native launch tracing - no-op
}
}

View File

@@ -21,6 +21,11 @@ export interface ShareExtensionTrace {
trace: string;
}
// TEMPORARY SHARE TARGET DIAGNOSTICS
export interface AppLaunchTrace {
trace: string;
}
export interface SharedImagePlugin {
/**
* Get shared image data from native layer
@@ -51,4 +56,16 @@ export interface SharedImagePlugin {
* Delete the Share Extension execution trace log if present (iOS)
*/
clearShareExtensionTrace(): Promise<void>;
// TEMPORARY SHARE TARGET DIAGNOSTICS
/**
* Read the raw app launch lifecycle trace log (iOS)
*/
getAppLaunchTrace(): Promise<AppLaunchTrace>;
// TEMPORARY SHARE TARGET DIAGNOSTICS
/**
* Delete the app launch lifecycle trace log if present (iOS)
*/
clearAppLaunchTrace(): Promise<void>;
}