@scalebun/react-native 1.2.1 → 1.2.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/android/src/main/java/com/scalebun/core/performance/AppStartCollector.kt +61 -13
  2. package/android/src/main/java/com/scalebun/rn/performance/PerformanceModule.kt +11 -0
  3. package/dist/scalebun.full.js +78 -9
  4. package/dist/scalebun.full.js.map +1 -1
  5. package/dist/scalebun.slim.js +78 -9
  6. package/dist/scalebun.slim.js.map +1 -1
  7. package/lib/commonjs/features/performance/PerformanceFeature.js +22 -2
  8. package/lib/commonjs/features/performance/PerformanceFeature.js.map +1 -1
  9. package/lib/commonjs/features/performance/collectors/AppLaunchCollector.js +21 -3
  10. package/lib/commonjs/features/performance/collectors/AppLaunchCollector.js.map +1 -1
  11. package/lib/commonjs/features/session/BackendSessionAdapter.js +41 -4
  12. package/lib/commonjs/features/session/BackendSessionAdapter.js.map +1 -1
  13. package/lib/module/features/performance/PerformanceFeature.js +22 -2
  14. package/lib/module/features/performance/PerformanceFeature.js.map +1 -1
  15. package/lib/module/features/performance/collectors/AppLaunchCollector.js +21 -3
  16. package/lib/module/features/performance/collectors/AppLaunchCollector.js.map +1 -1
  17. package/lib/module/features/session/BackendSessionAdapter.js +41 -4
  18. package/lib/module/features/session/BackendSessionAdapter.js.map +1 -1
  19. package/lib/typescript/features/performance/PerformanceFeature.d.ts.map +1 -1
  20. package/lib/typescript/features/performance/collectors/AppLaunchCollector.d.ts.map +1 -1
  21. package/lib/typescript/features/session/BackendSessionAdapter.d.ts.map +1 -1
  22. package/package.json +1 -1
  23. package/src/features/performance/PerformanceFeature.ts +19 -2
  24. package/src/features/performance/collectors/AppLaunchCollector.ts +17 -3
  25. package/src/features/session/BackendSessionAdapter.ts +32 -5
@@ -6,6 +6,7 @@ import android.os.Build
6
6
  import android.os.Bundle
7
7
  import android.os.Process
8
8
  import android.os.SystemClock
9
+ import android.util.Log
9
10
 
10
11
  /**
11
12
  * Captures app start timing using Android-native hooks.
@@ -23,10 +24,24 @@ import android.os.SystemClock
23
24
  */
24
25
  class AppStartCollector(private val app: Application) {
25
26
 
27
+ companion object {
28
+ private const val TAG = "ScaleBunPerformance"
29
+ /** Ignore an inferred cold-start proxy longer than this (stale/warm process). */
30
+ private const val MAX_INFERRED_DURATION_MS = 60_000L
31
+ }
32
+
26
33
  data class AppStartInfo(
27
34
  val launchType: String, // "cold" | "warm"
28
35
  val durationMs: Long,
29
- val breakdown: Breakdown?
36
+ val breakdown: Breakdown?,
37
+ /**
38
+ * true when the value was NOT captured from Activity lifecycle callbacks
39
+ * but inferred from process-start → query time. Happens on the common RN
40
+ * path where the SDK's native init runs (JS-driven) only after the launch
41
+ * activity has already resumed, so onActivityResumed is never observed.
42
+ * Without this fallback getInfo() returns null and no app_launch is emitted.
43
+ */
44
+ val inferred: Boolean = false
30
45
  )
31
46
 
32
47
  data class Breakdown(
@@ -86,9 +101,11 @@ class AppStartCollector(private val app: Application) {
86
101
  processStartMs = null, // Already at process start
87
102
  activityCreateMs = createDuration,
88
103
  firstFrameMs = resumeDuration
89
- )
104
+ ),
105
+ inferred = false
90
106
  )
91
107
 
108
+ Log.i(TAG, "app start captured from lifecycle: durationMs=$totalDuration")
92
109
  callback?.invoke(info)
93
110
  stop()
94
111
  }
@@ -113,20 +130,51 @@ class AppStartCollector(private val app: Application) {
113
130
  /** Check if app start has been captured */
114
131
  fun isCaptured(): Boolean = captured
115
132
 
116
- /** Get the captured info for on-demand queries */
133
+ /**
134
+ * Get app start info for on-demand queries.
135
+ *
136
+ * Prefers the precise value captured from Activity lifecycle callbacks. When
137
+ * capture never happened — the common RN case, because native init is driven
138
+ * from JS and runs only AFTER the launch activity resumed — falls back to an
139
+ * inferred duration from process start to now. Returning null here (the old
140
+ * behaviour) is exactly what caused App start = 0: AppLaunchCollector.collect()
141
+ * bails on a null/invalid duration, so no app_launch event was ever emitted.
142
+ */
117
143
  fun getInfo(): AppStartInfo? {
118
- if (!captured) return null
119
- val totalDuration = activityResumeUptimeMs - processStartUptimeMs
144
+ if (captured) {
145
+ val totalDuration = activityResumeUptimeMs - processStartUptimeMs
146
+ return AppStartInfo(
147
+ launchType = "cold",
148
+ durationMs = totalDuration,
149
+ breakdown = Breakdown(
150
+ processStartMs = null,
151
+ activityCreateMs = if (activityCreateUptimeMs > 0)
152
+ activityCreateUptimeMs - processStartUptimeMs else null,
153
+ firstFrameMs = if (activityCreateUptimeMs > 0)
154
+ activityResumeUptimeMs - activityCreateUptimeMs else null
155
+ ),
156
+ inferred = false
157
+ )
158
+ }
159
+
160
+ // Fallback: infer from process start → now. Guards against a bad clock
161
+ // (non-positive) and against a long-lived process where this proxy would
162
+ // be meaningless (e.g. queried minutes after a warm start).
163
+ if (processStartUptimeMs <= 0) {
164
+ Log.w(TAG, "app start not captured and no process-start clock — returning null")
165
+ return null
166
+ }
167
+ val inferredDuration = SystemClock.uptimeMillis() - processStartUptimeMs
168
+ if (inferredDuration <= 0 || inferredDuration > MAX_INFERRED_DURATION_MS) {
169
+ Log.w(TAG, "app start not captured; inferred duration out of range ($inferredDuration ms) — returning null")
170
+ return null
171
+ }
172
+ Log.i(TAG, "app start not captured from lifecycle; inferred durationMs=$inferredDuration")
120
173
  return AppStartInfo(
121
174
  launchType = "cold",
122
- durationMs = totalDuration,
123
- breakdown = Breakdown(
124
- processStartMs = null,
125
- activityCreateMs = if (activityCreateUptimeMs > 0)
126
- activityCreateUptimeMs - processStartUptimeMs else null,
127
- firstFrameMs = if (activityCreateUptimeMs > 0)
128
- activityResumeUptimeMs - activityCreateUptimeMs else null
129
- )
175
+ durationMs = inferredDuration,
176
+ breakdown = null,
177
+ inferred = true
130
178
  )
131
179
  }
132
180
  }
@@ -1,6 +1,7 @@
1
1
  package com.scalebun.rn.performance
2
2
 
3
3
  import android.app.Application
4
+ import android.util.Log
4
5
  import com.facebook.react.bridge.*
5
6
  import com.facebook.react.modules.core.DeviceEventManagerModule
6
7
  import com.scalebun.core.performance.AppStartCollector
@@ -33,6 +34,7 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
33
34
 
34
35
  companion object {
35
36
  const val NAME = "ScaleBunPerformance"
37
+ private const val TAG = "ScaleBunPerformance"
36
38
  }
37
39
 
38
40
  override fun getName(): String = NAME
@@ -65,6 +67,7 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
65
67
  putDouble("maxFrameTimeMs", snapshot.maxFrameTimeMs)
66
68
  putDouble("windowMs", snapshot.windowMs.toDouble())
67
69
  }
70
+ Log.i(TAG, "emit FrameMetrics: totalFrames=${snapshot.totalFrames} dropped=${snapshot.droppedFrames}")
68
71
  emit("ScaleBunPerformance_FrameMetrics", params)
69
72
  }
70
73
 
@@ -73,6 +76,7 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
73
76
  putDouble("durationMs", info.durationMs.toDouble())
74
77
  putDouble("timestamp", info.timestamp.toDouble())
75
78
  }
79
+ Log.i(TAG, "emit UiHang: durationMs=${info.durationMs}")
76
80
  emit("ScaleBunPerformance_UiHang", params)
77
81
  }
78
82
  }
@@ -83,12 +87,14 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
83
87
  override fun initialize(promise: Promise) {
84
88
  try {
85
89
  if (core?.isInitialized == true) {
90
+ Log.i(TAG, "initialize() — already initialized, no-op")
86
91
  promise.resolve(true)
87
92
  return
88
93
  }
89
94
 
90
95
  val app = reactApplicationContext.applicationContext as? Application
91
96
  if (app == null) {
97
+ Log.w(TAG, "initialize() — Application context not available")
92
98
  promise.reject("PERF_INIT_ERROR", "Application context not available")
93
99
  return
94
100
  }
@@ -98,8 +104,10 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
98
104
  performanceCore.initialize()
99
105
  core = performanceCore
100
106
 
107
+ Log.i(TAG, "initialize() — native performance core started")
101
108
  promise.resolve(true)
102
109
  } catch (e: Exception) {
110
+ Log.w(TAG, "initialize() — failed: ${e.message}")
103
111
  promise.reject("PERF_INIT_ERROR", e.message, e)
104
112
  }
105
113
  }
@@ -111,9 +119,11 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
111
119
  try {
112
120
  val info = core?.getAppStartInfo()
113
121
  if (info != null) {
122
+ Log.i(TAG, "getAppStartInfo() — launchType=${info.launchType} durationMs=${info.durationMs} inferred=${info.inferred}")
114
123
  val result = Arguments.createMap().apply {
115
124
  putString("launchType", info.launchType)
116
125
  putDouble("durationMs", info.durationMs.toDouble())
126
+ putBoolean("inferred", info.inferred)
117
127
  info.breakdown?.let { breakdown ->
118
128
  val breakdownMap = Arguments.createMap().apply {
119
129
  breakdown.processStartMs?.let { putDouble("processStartMs", it.toDouble()) }
@@ -125,6 +135,7 @@ class PerformanceModule(reactContext: ReactApplicationContext) : PerformanceSpec
125
135
  }
126
136
  promise.resolve(result)
127
137
  } else {
138
+ Log.w(TAG, "getAppStartInfo() — no info available (core=${if (core == null) "null" else "present"})")
128
139
  promise.resolve(null)
129
140
  }
130
141
  } catch (e: Exception) {
@@ -7480,6 +7480,7 @@ var init_AppLaunchCollector = __esm({
7480
7480
  "use strict";
7481
7481
  init_getNativePerformanceModule();
7482
7482
  init_models();
7483
+ init_internalLogger();
7483
7484
  JS_IMPORT_TIME = Date.now();
7484
7485
  AppLaunchCollector = class {
7485
7486
  collected = false;
@@ -7497,10 +7498,19 @@ var init_AppLaunchCollector = __esm({
7497
7498
  if (this.collected || !this.config.appLaunch) return;
7498
7499
  try {
7499
7500
  const perfModule = getNativePerformanceModule();
7500
- if (!perfModule?.getAppStartInfo) return;
7501
+ if (!perfModule?.getAppStartInfo) {
7502
+ logger.warn("[perf.trace] AppLaunchCollector: native perf module unavailable \u2014 no app_launch");
7503
+ return;
7504
+ }
7501
7505
  const jsReadyTimestamp = Date.now();
7502
7506
  const info = await perfModule.getAppStartInfo();
7503
- if (!info || typeof info.durationMs !== "number") return;
7507
+ if (!info || typeof info.durationMs !== "number") {
7508
+ logger.warn("[perf.trace] AppLaunchCollector: getAppStartInfo returned no usable duration \u2014 no app_launch", {
7509
+ hasInfo: !!info,
7510
+ durationType: typeof info?.durationMs
7511
+ });
7512
+ return;
7513
+ }
7504
7514
  this.collected = true;
7505
7515
  const breakdown = {};
7506
7516
  const phaseLabels = {};
@@ -7545,8 +7555,15 @@ var init_AppLaunchCollector = __esm({
7545
7555
  breakdown: hasBreakdown ? breakdown : void 0,
7546
7556
  phaseLabels: hasBreakdown ? phaseLabels : void 0
7547
7557
  };
7558
+ logger.warn("[perf.trace] AppLaunchCollector: app_launch built, invoking handler", {
7559
+ durationMs: event.durationMs,
7560
+ launchType: event.launchType
7561
+ });
7548
7562
  this.onLaunch(event);
7549
- } catch {
7563
+ } catch (e) {
7564
+ logger.warn("[perf.trace] AppLaunchCollector: collect() threw", {
7565
+ message: e?.message
7566
+ });
7550
7567
  }
7551
7568
  }
7552
7569
  };
@@ -8341,6 +8358,7 @@ var init_PerformanceFeature = __esm({
8341
8358
  init_PerformanceTransport();
8342
8359
  init_MetricsEngine();
8343
8360
  init_SessionManager();
8361
+ init_internalLogger();
8344
8362
  PerformanceFeature = class _PerformanceFeature {
8345
8363
  name = "performance";
8346
8364
  active = false;
@@ -8741,7 +8759,14 @@ var init_PerformanceFeature = __esm({
8741
8759
  * flushes the backlog in the order the metrics were produced.
8742
8760
  */
8743
8761
  _forwardMetricToBackend(metric) {
8744
- const backendTransport = SessionManager.getExistingInstance()?.getBackendTransport();
8762
+ const sm = SessionManager.getExistingInstance();
8763
+ const backendTransport = sm?.getBackendTransport();
8764
+ logger.warn("[perf.trace] forwardMetricToBackend", {
8765
+ type: metric.type,
8766
+ hasSessionManager: !!sm,
8767
+ hasBackendTransport: !!backendTransport,
8768
+ buffered: this._pendingBackendMetrics.length
8769
+ });
8745
8770
  if (backendTransport) {
8746
8771
  this._drainPendingBackendMetrics(backendTransport);
8747
8772
  backendTransport.queuePerformanceMetric(metric);
@@ -8761,6 +8786,9 @@ var init_PerformanceFeature = __esm({
8761
8786
  for (const metric of buffered) {
8762
8787
  backendTransport.queuePerformanceMetric(metric);
8763
8788
  }
8789
+ logger.warn("[perf.trace] drained buffered metrics to backend", {
8790
+ count: buffered.length
8791
+ });
8764
8792
  this._clearBackendDrainTimer();
8765
8793
  this._backendDrainAttempts = 0;
8766
8794
  }
@@ -8773,7 +8801,13 @@ var init_PerformanceFeature = __esm({
8773
8801
  */
8774
8802
  _scheduleBackendDrain() {
8775
8803
  if (this._backendDrainTimer) return;
8776
- if (this._backendDrainAttempts >= _PerformanceFeature.BACKEND_DRAIN_MAX_ATTEMPTS) return;
8804
+ if (this._backendDrainAttempts >= _PerformanceFeature.BACKEND_DRAIN_MAX_ATTEMPTS) {
8805
+ logger.warn("[perf.trace] drain gave up \u2014 buffered metrics never reached the backend", {
8806
+ attempts: this._backendDrainAttempts,
8807
+ dropped: this._pendingBackendMetrics.length
8808
+ });
8809
+ return;
8810
+ }
8777
8811
  const delays = _PerformanceFeature.BACKEND_DRAIN_DELAYS;
8778
8812
  const delay = delays[Math.min(this._backendDrainAttempts, delays.length - 1)];
8779
8813
  this._backendDrainAttempts++;
@@ -15749,12 +15783,23 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
15749
15783
  this._triggerNativeFlush();
15750
15784
  }
15751
15785
  queuePerformanceMetric(data) {
15752
- if (this.destroyed || !this.config.clientKey) return;
15786
+ if (this.destroyed || !this.config.clientKey) {
15787
+ logger.warn("[perf.queue] dropped \u2014 no clientKey or adapter destroyed", {
15788
+ type: data.type,
15789
+ destroyed: this.destroyed,
15790
+ hasClientKey: !!this.config.clientKey
15791
+ });
15792
+ return;
15793
+ }
15753
15794
  this.pendingPerformanceMetrics.push({
15754
15795
  ...data,
15755
15796
  clientId: this._clientId(),
15756
15797
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
15757
15798
  });
15799
+ logger.warn("[perf.trace] queue enqueued performance metric", {
15800
+ type: data.type,
15801
+ pending: this.pendingPerformanceMetrics.length
15802
+ });
15758
15803
  if (this.pendingPerformanceMetrics.length >= 20) {
15759
15804
  this._flushPerformance().catch(() => {
15760
15805
  });
@@ -16321,19 +16366,43 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
16321
16366
  }
16322
16367
  }
16323
16368
  async _flushPerformance() {
16324
- if (this.pendingPerformanceMetrics.length === 0 || !this.currentSessionId || !this.config.clientKey) return;
16369
+ if (this.pendingPerformanceMetrics.length === 0) return;
16370
+ if (!this.currentSessionId || !this.config.clientKey) {
16371
+ logger.warn("[perf.trace] flush waiting \u2014 not deliverable yet", {
16372
+ pending: this.pendingPerformanceMetrics.length,
16373
+ hasSession: !!this.currentSessionId,
16374
+ hasClientKey: !!this.config.clientKey
16375
+ });
16376
+ return;
16377
+ }
16325
16378
  const batch = this.pendingPerformanceMetrics.splice(0, 50);
16326
16379
  try {
16327
- if (await this._enqueueNative("performance", batch)) return;
16380
+ if (await this._enqueueNative("performance", batch)) {
16381
+ logger.warn("[perf.trace] flush handed to native outbox", {
16382
+ count: batch.length
16383
+ });
16384
+ return;
16385
+ }
16328
16386
  if (!this._sessionRowReady()) {
16387
+ logger.warn("[perf.trace] flush session row not ready \u2014 re-buffering", {
16388
+ count: batch.length
16389
+ });
16329
16390
  this.pendingPerformanceMetrics.unshift(...batch);
16330
16391
  return;
16331
16392
  }
16332
16393
  await this._post(ENDPOINTS.INGESTION_PERFORMANCE(this.currentSessionId), {
16333
16394
  metrics: batch
16334
16395
  });
16396
+ logger.warn("[perf.trace] flush POST performance metrics ok", {
16397
+ count: batch.length
16398
+ });
16335
16399
  } catch (err) {
16336
- if (!_BackendSessionAdapter._isPermanentHttpError(err)) this.pendingPerformanceMetrics.unshift(...batch);
16400
+ const permanent = _BackendSessionAdapter._isPermanentHttpError(err);
16401
+ logger.warn("[perf.flush] POST performance metrics failed", {
16402
+ count: batch.length,
16403
+ permanent
16404
+ });
16405
+ if (!permanent) this.pendingPerformanceMetrics.unshift(...batch);
16337
16406
  }
16338
16407
  }
16339
16408
  async _flushLogs() {