@scalebun/react-native 1.10.7 → 1.11.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 (54) hide show
  1. package/android/src/main/java/com/scalebun/replaysdk/tracking/InteractionTracker.kt +25 -25
  2. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +15 -3
  3. package/dist/scalebun.full.js +467 -255
  4. package/dist/scalebun.slim.js +466 -254
  5. package/ios/Capture/InteractionTracker.swift +8 -4
  6. package/ios/Ota/OtaSlotManager.swift +19 -5
  7. package/lib/commonjs/analytics/EventTracker.js +5 -5
  8. package/lib/commonjs/analytics/automaticEvents.js +3 -2
  9. package/lib/commonjs/core/constants/version.js +7 -2
  10. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +80 -103
  11. package/lib/commonjs/features/journey/interactionProtocol.js +47 -0
  12. package/lib/commonjs/features/journey/uiState.js +8 -1
  13. package/lib/commonjs/features/ota/OtaOrchestrator.js +174 -48
  14. package/lib/commonjs/features/ota/OtaTypes.js +4 -0
  15. package/lib/commonjs/features/ota/useOtaUpdate.js +11 -2
  16. package/lib/commonjs/features/session/JourneyEventPipeline.js +6 -5
  17. package/lib/commonjs/features/session/SessionManager.js +37 -38
  18. package/lib/commonjs/public/ScaleBunFacade.js +115 -2
  19. package/lib/module/analytics/EventTracker.js +5 -5
  20. package/lib/module/analytics/automaticEvents.js +3 -2
  21. package/lib/module/core/constants/version.js +7 -2
  22. package/lib/module/features/journey/ScaleBunDebugRoot.js +80 -103
  23. package/lib/module/features/journey/interactionProtocol.js +38 -0
  24. package/lib/module/features/journey/uiState.js +8 -1
  25. package/lib/module/features/ota/OtaOrchestrator.js +174 -48
  26. package/lib/module/features/ota/OtaTypes.js +1 -1
  27. package/lib/module/features/ota/useOtaUpdate.js +11 -2
  28. package/lib/module/features/session/JourneyEventPipeline.js +6 -5
  29. package/lib/module/features/session/SessionManager.js +37 -38
  30. package/lib/module/public/ScaleBunFacade.js +115 -2
  31. package/lib/typescript/analytics/EventTracker.d.ts +1 -1
  32. package/lib/typescript/analytics/automaticEvents.d.ts +3 -1
  33. package/lib/typescript/core/constants/version.d.ts +7 -2
  34. package/lib/typescript/features/journey/interactionProtocol.d.ts +21 -0
  35. package/lib/typescript/features/ota/OtaEventEmitter.d.ts +15 -1
  36. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +22 -3
  37. package/lib/typescript/features/ota/OtaTypes.d.ts +29 -32
  38. package/lib/typescript/features/session/JourneyEventPipeline.d.ts +1 -0
  39. package/lib/typescript/features/session/SessionManager.d.ts +15 -10
  40. package/lib/typescript/public/ScaleBunFacade.d.ts +27 -0
  41. package/package.json +4 -3
  42. package/src/analytics/EventTracker.ts +5 -5
  43. package/src/analytics/automaticEvents.ts +4 -0
  44. package/src/core/constants/version.ts +7 -2
  45. package/src/features/journey/ScaleBunDebugRoot.tsx +96 -97
  46. package/src/features/journey/interactionProtocol.ts +65 -0
  47. package/src/features/journey/uiState.ts +9 -4
  48. package/src/features/ota/OtaEventEmitter.ts +12 -0
  49. package/src/features/ota/OtaOrchestrator.ts +209 -62
  50. package/src/features/ota/OtaTypes.ts +37 -39
  51. package/src/features/ota/useOtaUpdate.ts +11 -2
  52. package/src/features/session/JourneyEventPipeline.ts +7 -5
  53. package/src/features/session/SessionManager.ts +75 -38
  54. package/src/public/ScaleBunFacade.ts +127 -3
@@ -28,9 +28,14 @@ import React, {
28
28
  type ReactNode,
29
29
  } from 'react';
30
30
  import { View, NativeEventEmitter, NativeModules } from 'react-native';
31
- import { emitAutomaticEvent } from '../../analytics/automaticEvents';
32
31
  import { resolveTouchTarget, describeTouchTarget } from './touchTarget';
33
32
  import { resolvePlatformOS } from '../../core/context/device';
33
+ import {
34
+ INTERACTION_PROTOCOL_VERSION,
35
+ generateInteractionId,
36
+ nearestInteractionStart,
37
+ type InteractionStartContext,
38
+ } from './interactionProtocol';
34
39
 
35
40
  // ─── Lazy imports to avoid native module cascade at load time ────────────────
36
41
  // We only import TYPE references at the top level; actual modules are require()'d
@@ -60,6 +65,11 @@ interface ScaleBunDebugRootProps {
60
65
  suppressDeprecationWarning?: boolean;
61
66
  }
62
67
 
68
+ interface TouchStartContext extends InteractionStartContext {
69
+ locationX?: number;
70
+ locationY?: number;
71
+ }
72
+
63
73
  export function ScaleBunDebugRoot({
64
74
  children,
65
75
  navigationRef,
@@ -270,7 +280,9 @@ export function ScaleBunDebugRoot({
270
280
  // and forwards them to SessionManager as USER_ACTION events.
271
281
  const lastNativeTouchTsRef = useRef<number>(0);
272
282
  const nativeTrackingConfirmedRef = useRef<boolean>(false);
273
- const pendingJsEmitRef = useRef<ReturnType<typeof setTimeout> | null>(null);
283
+ const touchStartRef = useRef<TouchStartContext | null>(null);
284
+ const interactionStartsRef = useRef<TouchStartContext[]>([]);
285
+ const pendingJsEmitsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
274
286
 
275
287
  useEffect(() => {
276
288
  let subscription: any = null;
@@ -284,10 +296,19 @@ export function ScaleBunDebugRoot({
284
296
  lastNativeTouchTsRef.current = Date.now();
285
297
  nativeTrackingConfirmedRef.current = true;
286
298
 
287
- // Cancel any pending JS emission — native event wins
288
- if (pendingJsEmitRef.current !== null) {
289
- clearTimeout(pendingJsEmitRef.current);
290
- pendingJsEmitRef.current = null;
299
+ const nativeOccurredAt =
300
+ typeof event.occurredAt === 'number'
301
+ ? event.occurredAt
302
+ : (typeof event.timestamp === 'number' ? event.timestamp : Date.now()) -
303
+ (typeof event.durationMs === 'number' ? event.durationMs : 0);
304
+ const start = nearestInteractionStart(interactionStartsRef.current, nativeOccurredAt);
305
+
306
+ // Cancel only this physical touch's fallback. A single timer used to cancel a
307
+ // previous rapid tap and silently lose it on devices without native tracking.
308
+ if (start) {
309
+ const pending = pendingJsEmitsRef.current.get(start.interactionId);
310
+ if (pending !== undefined) clearTimeout(pending);
311
+ pendingJsEmitsRef.current.delete(start.interactionId);
291
312
  }
292
313
 
293
314
  const { SessionManager } = require('../session/SessionManager');
@@ -306,6 +327,15 @@ export function ScaleBunDebugRoot({
306
327
  } catch { /* no-throw */ }
307
328
 
308
329
  (sm as any).onGestureDetected(event.gestureType || 'tap', {
330
+ interactionId: event.interactionId || start?.interactionId || generateInteractionId(),
331
+ interactionProtocol: event.interactionProtocol || INTERACTION_PROTOCOL_VERSION,
332
+ occurredAt: nativeOccurredAt,
333
+ ui: start?.ui,
334
+ stateStatus: start?.stateStatus ?? 'not_captured',
335
+ target: start?.target,
336
+ targetId: start?.targetId,
337
+ screenName: start?.screenName,
338
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
309
339
  x: event.rawX,
310
340
  y: event.rawY,
311
341
  // Use the actual native end coords when present, not the down
@@ -334,11 +364,12 @@ export function ScaleBunDebugRoot({
334
364
 
335
365
  return () => {
336
366
  try { subscription?.remove(); } catch { /* no-throw */ }
367
+ for (const timer of pendingJsEmitsRef.current.values()) clearTimeout(timer);
368
+ pendingJsEmitsRef.current.clear();
337
369
  };
338
- }, []);
370
+ }, [captureAutomaticInteractions]);
339
371
 
340
372
  // ─── Touch Tracking State ───────────────────────────────────────────
341
- const touchStartRef = useRef<{ x: number; y: number; ts: number } | null>(null);
342
373
 
343
374
  // On-screen rect of the root view — the EXACT view the native screenshot
344
375
  // captures. Normalizing pageX/pageY against this rect makes JS-fallback taps
@@ -377,7 +408,36 @@ export function ScaleBunDebugRoot({
377
408
  const handleTouchStart = (e: any) => {
378
409
  try {
379
410
  const touch = e.nativeEvent;
380
- touchStartRef.current = { x: touch.pageX, y: touch.pageY, ts: Date.now() };
411
+ const target = resolveTouchTarget(e);
412
+ let ui: string | undefined;
413
+ let screenName: string | undefined;
414
+ try {
415
+ const { uiStateSignature } = require('./uiState');
416
+ ui = uiStateSignature();
417
+ } catch { /* no-throw */ }
418
+ try {
419
+ const { AutoScreenDetector } = require('../navigation/AutoScreenDetector');
420
+ screenName = AutoScreenDetector.getInstance().getCurrentScreen() || undefined;
421
+ } catch { /* no-throw */ }
422
+ const start: TouchStartContext = {
423
+ interactionId: generateInteractionId(),
424
+ occurredAt: Date.now(),
425
+ x: touch.pageX,
426
+ y: touch.pageY,
427
+ locationX: touch.locationX,
428
+ locationY: touch.locationY,
429
+ target: describeTouchTarget(target),
430
+ targetId: target?.testID,
431
+ screenName,
432
+ ui,
433
+ stateStatus: ui ? 'captured_nonempty' : 'not_instrumented',
434
+ emitAutomaticAnalytics: captureAutomaticInteractions,
435
+ };
436
+ touchStartRef.current = start;
437
+ interactionStartsRef.current = interactionStartsRef.current
438
+ .filter((candidate) => start.occurredAt - candidate.occurredAt < 5000)
439
+ .concat(start)
440
+ .slice(-8);
381
441
  } catch { /* no-throw */ }
382
442
  };
383
443
 
@@ -413,16 +473,24 @@ export function ScaleBunDebugRoot({
413
473
  // WHAT was tapped, resolved from the React fiber on the touch event. Without this the tap
414
474
  // carries only coordinates, so every tap in the app groups into one row per gesture type.
415
475
  // Defensive by construction — returns undefined rather than throwing (see touchTarget.ts).
416
- const tapped = describeTouchTarget(resolveTouchTarget(e));
476
+ const tapped = start?.target ?? describeTouchTarget(resolveTouchTarget(e));
417
477
 
418
478
  const gestureDetails: any = {
419
479
  target: tapped,
420
- x: touch.pageX,
421
- y: touch.pageY,
422
- pageX: touch.pageX,
423
- pageY: touch.pageY,
424
- locationX: touch.locationX,
425
- locationY: touch.locationY,
480
+ interactionId: start?.interactionId ?? generateInteractionId(),
481
+ interactionProtocol: INTERACTION_PROTOCOL_VERSION,
482
+ occurredAt: start?.occurredAt ?? Date.now(),
483
+ ui: start?.ui,
484
+ stateStatus: start?.stateStatus ?? 'not_captured',
485
+ targetId: start?.targetId,
486
+ screenName: start?.screenName,
487
+ emitAutomaticAnalytics: start?.emitAutomaticAnalytics ?? captureAutomaticInteractions,
488
+ x: start?.x ?? touch.pageX,
489
+ y: start?.y ?? touch.pageY,
490
+ pageX: start?.x ?? touch.pageX,
491
+ pageY: start?.y ?? touch.pageY,
492
+ locationX: start?.locationX ?? touch.locationX,
493
+ locationY: start?.locationY ?? touch.locationY,
426
494
  viewportWidth: vpW > 0 ? vpW : undefined,
427
495
  viewportHeight: vpH > 0 ? vpH : undefined,
428
496
  };
@@ -434,8 +502,8 @@ export function ScaleBunDebugRoot({
434
502
  // measureInWindow returns rootRect.y = statusBarHeight, so subtracting it
435
503
  // double-counts the status bar and pushes every marker upward.
436
504
  if (rootRect && rootRect.w > 0 && rootRect.h > 0) {
437
- const nx = Math.max(0, Math.min(1, touch.pageX / rootRect.w));
438
- const ny = Math.max(0, Math.min(1, touch.pageY / rootRect.h));
505
+ const nx = Math.max(0, Math.min(1, (start?.x ?? touch.pageX) / rootRect.w));
506
+ const ny = Math.max(0, Math.min(1, (start?.y ?? touch.pageY) / rootRect.h));
439
507
  gestureDetails.normalizedX = nx;
440
508
  gestureDetails.normalizedY = ny;
441
509
  gestureDetails.normalizedPrecomputed = true;
@@ -472,7 +540,7 @@ export function ScaleBunDebugRoot({
472
540
  const dx = touch.pageX - start.x;
473
541
  const dy = touch.pageY - start.y;
474
542
  const dist = Math.sqrt(dx * dx + dy * dy);
475
- const duration = Date.now() - start.ts;
543
+ const duration = Date.now() - start.occurredAt;
476
544
  gestureDetails.duration = duration;
477
545
 
478
546
  if (dist >= 15) {
@@ -494,66 +562,17 @@ export function ScaleBunDebugRoot({
494
562
  }
495
563
  }
496
564
 
497
- const targetInfo = _extractTarget(e);
498
- gestureDetails.target = targetInfo?.testId || targetInfo?.accessibilityLabel;
499
-
500
- if (captureAutomaticInteractions) {
501
- let screen: string | undefined;
502
- try {
503
- const { AutoScreenDetector } = require('../navigation/AutoScreenDetector');
504
- screen = AutoScreenDetector.getInstance().getCurrentScreen() || undefined;
505
- } catch { /* no-throw */ }
506
- /**
507
- * UI STATE, read HERE and not later.
508
- *
509
- * A tap belongs to the surface that was on screen when the finger landed: tapping the
510
- * filter button while the sheet is DOWN belongs to `closed`, because that is what the
511
- * user was looking at when they reached for it. Reading it after the handler has run
512
- * moves every "open the thing" tap into the state it created — the one state it
513
- * certainly does not belong to.
514
- *
515
- * Declared-only on this platform (see uiState.ts): absent means not captured, which
516
- * the dashboard keeps distinct from "nothing was open".
517
- */
518
- let ui: string | undefined;
519
- try {
520
- const { uiStateSignature } = require('./uiState');
521
- ui = uiStateSignature();
522
- } catch { /* no-throw: a tap must never be lost to state capture */ }
523
- emitAutomaticEvent('element_interacted', {
524
- gesture_type: gestureType,
525
- screen_name: screen,
526
- /* THIS MAP ENUMERATES. A field added to the payload and forgotten here reaches the
527
- backend as undefined with no error anywhere — the recurring defect class in this
528
- codebase. `ui` is the key the grid aggregate reads for state, byte-identical to
529
- the web SDK's, so one dashboard control queries both platforms. */
530
- ui,
531
- // testID/nativeID is an author-controlled stable identifier.
532
- // Accessibility labels and rendered text are deliberately omitted.
533
- target_id: targetInfo?.testId,
534
- normalized_x: gestureDetails.normalizedX,
535
- normalized_y: gestureDetails.normalizedY,
536
- direction: gestureDetails.direction,
537
- duration_ms: gestureDetails.duration,
538
- });
539
- }
540
-
541
565
  // Feed the Engage gesture detector (rage_tap / dead_tap) — only committed
542
566
  // taps, not swipes/scrolls/long-presses. Pure JS, cross-platform; emits the
543
567
  // unified `$gesture` signal the engage trigger engine listens for.
544
568
  if (gestureType === 'tap') {
545
569
  try {
546
570
  const { gestureTriggerDetector } = require('../engage/gestureTriggerDetector');
547
- let screen: string | undefined;
548
- try {
549
- const { AutoScreenDetector } = require('../navigation/AutoScreenDetector');
550
- screen = AutoScreenDetector.getInstance().getCurrentScreen() || undefined;
551
- } catch { /* no-throw */ }
552
571
  gestureTriggerDetector.recordTap({
553
- x: touch.pageX,
554
- y: touch.pageY,
555
- hasTarget: !!(targetInfo?.testId || targetInfo?.accessibilityLabel),
556
- screen,
572
+ x: start?.x ?? touch.pageX,
573
+ y: start?.y ?? touch.pageY,
574
+ hasTarget: !!(start?.targetId || start?.target),
575
+ screen: start?.screenName,
557
576
  });
558
577
  } catch { /* no-throw */ }
559
578
  }
@@ -568,7 +587,7 @@ export function ScaleBunDebugRoot({
568
587
  // screenshot capture view). Three-tier suppression:
569
588
  // 1. Native confirmed active → skip JS entirely (native handles all touches)
570
589
  // 2. Native event within 300ms → skip (existing dedup)
571
- // 3. First touch (native not yet confirmed) → defer 200ms to let native arrive
590
+ // 3. First touch (native not yet confirmed) → defer 600ms to let native arrive
572
591
  const sinceNative = Date.now() - lastNativeTouchTsRef.current;
573
592
  if (nativeTrackingConfirmedRef.current) {
574
593
  // Native tracker is active — skip JS path entirely
@@ -579,11 +598,9 @@ export function ScaleBunDebugRoot({
579
598
  // If native fires during the delay, it cancels this pending emit.
580
599
  const capturedType = gestureType;
581
600
  const capturedDetails = { ...gestureDetails };
582
- if (pendingJsEmitRef.current !== null) {
583
- clearTimeout(pendingJsEmitRef.current);
584
- }
585
- pendingJsEmitRef.current = setTimeout(() => {
586
- pendingJsEmitRef.current = null;
601
+ const interactionId = capturedDetails.interactionId as string;
602
+ const timer = setTimeout(() => {
603
+ pendingJsEmitsRef.current.delete(interactionId);
587
604
  if (nativeTrackingConfirmedRef.current) return;
588
605
  try {
589
606
  const { SessionManager } = require('../session/SessionManager');
@@ -605,6 +622,7 @@ export function ScaleBunDebugRoot({
605
622
  // devices with no native tracker at all it merely delays a background emission,
606
623
  // which nothing user-visible waits on.
607
624
  }, 600);
625
+ pendingJsEmitsRef.current.set(interactionId, timer);
608
626
  }
609
627
 
610
628
  touchStartRef.current = null;
@@ -724,25 +742,6 @@ export function useScaleBunScreen(name: string): void {
724
742
  }, [name, manager]);
725
743
  }
726
744
 
727
- // ─── Helpers ────────────────────────────────────────────────────────────────
728
-
729
- function _extractTarget(e: any): { testId?: string; accessibilityLabel?: string; text?: string } | undefined {
730
- try {
731
- const target = e?.target;
732
- if (!target) return undefined;
733
-
734
- // React Native nativeEvent target properties
735
- const props = target._internalFiberInstanceHandleDEV?.memoizedProps ?? {};
736
- return {
737
- testId: props.testID || props.nativeID || undefined,
738
- accessibilityLabel: props.accessibilityLabel || undefined,
739
- text: undefined,
740
- };
741
- } catch {
742
- return undefined;
743
- }
744
- }
745
-
746
745
  // ─── Styles ─────────────────────────────────────────────────────────────────
747
746
  // IMPORTANT: Do NOT use StyleSheet.create() here.
748
747
  // In RN 0.84 bridgeless mode, StyleSheet.create() calls
@@ -0,0 +1,65 @@
1
+ /** One physical interaction, shared by replay and analytics projections. */
2
+ export const INTERACTION_PROTOCOL_VERSION = 1;
3
+
4
+ export type InteractionStateStatus =
5
+ | 'captured_empty'
6
+ | 'captured_nonempty'
7
+ | 'not_captured'
8
+ | 'not_instrumented';
9
+
10
+ export interface InteractionStartContext {
11
+ interactionId: string;
12
+ occurredAt: number;
13
+ x: number;
14
+ y: number;
15
+ target?: string;
16
+ targetId?: string;
17
+ screenName?: string;
18
+ ui?: string;
19
+ stateStatus: InteractionStateStatus;
20
+ emitAutomaticAnalytics: boolean;
21
+ }
22
+
23
+ export function generateInteractionId(): string {
24
+ return `ixj-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
25
+ }
26
+
27
+ /** Match a native bridge event to the JS evidence sampled at the same finger-down. */
28
+ export function nearestInteractionStart<T extends InteractionStartContext>(
29
+ starts: readonly T[],
30
+ occurredAt: number,
31
+ toleranceMs = 1500,
32
+ ): T | undefined {
33
+ let best: T | undefined;
34
+ let bestDelta = toleranceMs + 1;
35
+ for (const start of starts) {
36
+ const delta = Math.abs(start.occurredAt - occurredAt);
37
+ if (delta < bestDelta) {
38
+ best = start;
39
+ bestDelta = delta;
40
+ }
41
+ }
42
+ return bestDelta <= toleranceMs ? best : undefined;
43
+ }
44
+
45
+ /** Analytics is a projection of the same evidence; no second click is invented. */
46
+ export function automaticInteractionProperties(
47
+ payload: Record<string, unknown>,
48
+ screenName: string | undefined,
49
+ canonicalMirror: boolean,
50
+ ): Record<string, unknown> {
51
+ return {
52
+ gesture_type: payload.gestureType,
53
+ screen_name: screenName,
54
+ interaction_id: payload.interaction_id,
55
+ interaction_protocol: payload.interaction_protocol,
56
+ state_status: payload.state_status,
57
+ ui: payload.ui,
58
+ target_id: payload.target_id,
59
+ normalized_x: payload.normalizedX,
60
+ normalized_y: payload.normalizedY,
61
+ direction: payload.direction,
62
+ duration_ms: payload.durationMs,
63
+ canonical_mirror: canonicalMirror || undefined,
64
+ };
65
+ }
@@ -76,9 +76,14 @@ export function uiStateSignature(): string | undefined {
76
76
  if (!declared.size) return undefined;
77
77
  /* SORTED, or the same surface produces different signatures depending on the order the host happened
78
78
  to declare things in, and every count fragments into several that mean nothing. */
79
- return [...declared.keys()]
79
+ const pairs = [...declared.keys()]
80
80
  .sort()
81
- .map((k) => `${k}:${declared.get(k)}`)
82
- .join(';')
83
- .slice(0, 96);
81
+ .map((k) => `${k}:${declared.get(k)}`);
82
+ let signature = '';
83
+ for (const pair of pairs) {
84
+ const next = signature ? `${signature};${pair}` : pair;
85
+ if (next.length > 96) break;
86
+ signature = next;
87
+ }
88
+ return signature || undefined;
84
89
  }
@@ -9,11 +9,17 @@
9
9
  // ── Event types ───────────────────────────────────────────────────────────────
10
10
 
11
11
  export type OtaEventType =
12
+ /** A check was made against the server. The denominator of the funnel. */
12
13
  | 'CHECK'
14
+ /** The server answered with a bundle for this device — the offer, before any bytes move. */
15
+ | 'OFFERED'
13
16
  | 'DOWNLOAD_STARTED'
14
17
  | 'DOWNLOAD_PROGRESS'
15
18
  | 'DOWNLOAD_COMPLETE'
19
+ /** Staged and swapped. Emitted optimistically, BEFORE the bundle has booted. */
16
20
  | 'INSTALLED'
21
+ /** The bundle booted and survived to the healthy mark — the honest activation signal. */
22
+ | 'BOOT_SUCCESS'
17
23
  | 'APPLY_FAILED'
18
24
  | 'AUTO_ROLLBACK'
19
25
  | 'MANUAL_ROLLBACK';
@@ -21,6 +27,12 @@ export type OtaEventType =
21
27
  export interface OtaEvent {
22
28
  type: OtaEventType;
23
29
  bundleId: string;
30
+ /**
31
+ * The release this bundle was served as. Optional because a bundle installed
32
+ * by an older SDK has no recorded release; present on everything emitted by a
33
+ * current one, so the delivery funnel can key by release rather than bundle.
34
+ */
35
+ releaseId?: string;
24
36
  version?: number;
25
37
  /** 0–100 for DOWNLOAD_PROGRESS */
26
38
  progress?: number;