@scalebun/react-native 1.4.0 → 1.6.0

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 (50) hide show
  1. package/README.md +33 -0
  2. package/android/src/main/java/com/scalebun/rn/ota/BsPatch.kt +182 -0
  3. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +88 -13
  4. package/android/src/oldarch/java/com/scalebun/rn/crash/ScaleBunCrashSpec.kt +16 -0
  5. package/bin/lib/androidCodemod.js +370 -0
  6. package/bin/scalebun.js +305 -13
  7. package/dist/scalebun.full.js +129 -97
  8. package/dist/scalebun.full.js.map +1 -1
  9. package/dist/scalebun.slim.js +129 -97
  10. package/dist/scalebun.slim.js.map +1 -1
  11. package/ios/Ota/BsPatch.swift +180 -0
  12. package/ios/Ota/OtaSlotManager.swift +59 -2
  13. package/ios/Ota/ScaleBunOtaModule.swift +89 -10
  14. package/lib/commonjs/analytics/EventTracker.js +21 -1
  15. package/lib/commonjs/analytics/EventTracker.js.map +1 -1
  16. package/lib/commonjs/core/constants/version.js +1 -1
  17. package/lib/commonjs/core/id/installationId.js +55 -0
  18. package/lib/commonjs/core/id/installationId.js.map +1 -0
  19. package/lib/commonjs/debug/bootstrap.js +12 -0
  20. package/lib/commonjs/debug/bootstrap.js.map +1 -1
  21. package/lib/commonjs/features/ota/OtaOrchestrator.js +2 -30
  22. package/lib/commonjs/features/ota/OtaOrchestrator.js.map +1 -1
  23. package/lib/commonjs/features/session/BackendSessionAdapter.js +8 -0
  24. package/lib/commonjs/features/session/BackendSessionAdapter.js.map +1 -1
  25. package/lib/module/analytics/EventTracker.js +21 -1
  26. package/lib/module/analytics/EventTracker.js.map +1 -1
  27. package/lib/module/core/constants/version.js +1 -1
  28. package/lib/module/core/id/installationId.js +50 -0
  29. package/lib/module/core/id/installationId.js.map +1 -0
  30. package/lib/module/debug/bootstrap.js +12 -0
  31. package/lib/module/debug/bootstrap.js.map +1 -1
  32. package/lib/module/features/ota/OtaOrchestrator.js +1 -29
  33. package/lib/module/features/ota/OtaOrchestrator.js.map +1 -1
  34. package/lib/module/features/session/BackendSessionAdapter.js +8 -0
  35. package/lib/module/features/session/BackendSessionAdapter.js.map +1 -1
  36. package/lib/typescript/analytics/EventTracker.d.ts.map +1 -1
  37. package/lib/typescript/core/constants/version.d.ts +1 -1
  38. package/lib/typescript/core/id/installationId.d.ts +20 -0
  39. package/lib/typescript/core/id/installationId.d.ts.map +1 -0
  40. package/lib/typescript/debug/bootstrap.d.ts +18 -0
  41. package/lib/typescript/debug/bootstrap.d.ts.map +1 -1
  42. package/lib/typescript/features/ota/OtaOrchestrator.d.ts.map +1 -1
  43. package/lib/typescript/features/session/BackendSessionAdapter.d.ts.map +1 -1
  44. package/package.json +3 -2
  45. package/src/analytics/EventTracker.ts +21 -1
  46. package/src/core/constants/version.ts +1 -1
  47. package/src/core/id/installationId.ts +52 -0
  48. package/src/debug/bootstrap.ts +36 -0
  49. package/src/features/ota/OtaOrchestrator.ts +1 -30
  50. package/src/features/session/BackendSessionAdapter.ts +8 -0
package/bin/scalebun.js CHANGED
@@ -22,6 +22,7 @@
22
22
  const fs = require('fs');
23
23
  const path = require('path');
24
24
  const codemod = require('./lib/iosCodemod');
25
+ const androidCodemod = require('./lib/androidCodemod');
25
26
 
26
27
  const MIN_BRIDGELESS_RNFIREBASE_MAJOR = 18;
27
28
 
@@ -597,6 +598,215 @@ function runInitIos(argv) {
597
598
  process.exit(0);
598
599
  }
599
600
 
601
+ // ─── `init android` — MainApplication OTA wiring + checklist ──────────────────
602
+
603
+ function findAndroidDir() {
604
+ const androidDir = path.join(CWD, 'android');
605
+ return exists(androidDir) ? androidDir : null;
606
+ }
607
+
608
+ /** [major, minor] of the installed react-native, or null. */
609
+ function rnMinor() {
610
+ const v = pkgVersion('react-native');
611
+ if (!v) return null;
612
+ const parts = v.split('.').map((n) => parseInt(n, 10));
613
+ return [parts[0] || 0, parts[1] || 0];
614
+ }
615
+
616
+ /**
617
+ * Detect Android setup that the codemod cannot apply — the checklist items
618
+ * (Firebase config, gradle plugin, manifest permissions, the New-Arch OTA
619
+ * version gate). Mirrors detectIosCapabilities.
620
+ */
621
+ function detectAndroidCapabilities(androidDir) {
622
+ const rnVersion = pkgVersion('react-native');
623
+ const minor = rnMinor();
624
+
625
+ const gradleProps = readText(path.join(androidDir, 'gradle.properties')) || '';
626
+ const newArch = /(^|\n)\s*newArchEnabled\s*=\s*true/.test(gradleProps);
627
+
628
+ // Bridgeless ignored getJSBundleFile() until RN 0.76.1 — an OTA update installs
629
+ // and silently never runs on 0.74/0.75/0.76.0 with New Arch on.
630
+ const otaDeadZone =
631
+ newArch &&
632
+ minor != null &&
633
+ minor[0] === 0 &&
634
+ (minor[1] === 74 || minor[1] === 75 || (rnVersion || '').startsWith('0.76.0'));
635
+
636
+ const hasGoogleServicesJson = exists(path.join(androidDir, 'app', 'google-services.json'));
637
+
638
+ const manifestPath = path.join(androidDir, 'app', 'src', 'main', 'AndroidManifest.xml');
639
+ const manifest = readText(manifestPath) || '';
640
+ const hasPostNotifications = /android\.permission\.POST_NOTIFICATIONS/.test(manifest);
641
+ const hasChannelMeta = /default_notification_channel_id/.test(manifest);
642
+
643
+ return {
644
+ rnVersion,
645
+ newArch,
646
+ otaDeadZone,
647
+ hasGoogleServicesJson,
648
+ manifestExists: !!readText(manifestPath),
649
+ hasPostNotifications,
650
+ hasChannelMeta,
651
+ };
652
+ }
653
+
654
+ function printAndroidChecklist(caps) {
655
+ console.log('');
656
+ console.log(paint('Android setup (verify / apply by hand — cannot be safely automated)', C.bold));
657
+
658
+ if (caps.otaDeadZone) {
659
+ console.log(
660
+ ' ' + FAIL() + ' ' +
661
+ paint(
662
+ `React Native ${caps.rnVersion} + New Architecture: OTA updates CANNOT LOAD.`,
663
+ C.red,
664
+ ),
665
+ );
666
+ console.log(
667
+ paint(
668
+ ' Bridgeless ignored getJSBundleFile() until RN 0.76.1. Upgrade to >= 0.76.1,\n' +
669
+ ' or set newArchEnabled=false. Until then updates install but never run.',
670
+ C.dim,
671
+ ),
672
+ );
673
+ }
674
+
675
+ line(caps.hasGoogleServicesJson ? OK() : WARN(), 'google-services.json', caps.hasGoogleServicesJson ? 'present' : null);
676
+ line(caps.hasPostNotifications ? OK() : WARN(), 'POST_NOTIFICATIONS perm', caps.hasPostNotifications ? 'declared' : null);
677
+ line(caps.hasChannelMeta ? OK() : WARN(), 'default channel meta', caps.hasChannelMeta ? 'declared' : null);
678
+
679
+ const todo = [];
680
+ if (!caps.hasGoogleServicesJson) {
681
+ todo.push(
682
+ 'For push: add ' + paint('android/app/google-services.json', C.cyan) +
683
+ ' (Firebase console), apply the Google Services gradle plugin, and add ' +
684
+ paint('@react-native-firebase/messaging', C.cyan) + ' for real tokens.',
685
+ );
686
+ }
687
+ if (caps.manifestExists && !caps.hasPostNotifications) {
688
+ todo.push(
689
+ 'Declare the Android 13+ permission in AndroidManifest.xml: ' +
690
+ paint('<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />', C.cyan),
691
+ );
692
+ }
693
+ if (caps.manifestExists && !caps.hasChannelMeta) {
694
+ todo.push(
695
+ 'Declare a default FCM channel meta-data ' +
696
+ '(com.google.firebase.messaging.default_notification_channel_id) so backgrounded ' +
697
+ 'notifications have a channel.',
698
+ );
699
+ }
700
+
701
+ if (todo.length) {
702
+ console.log('');
703
+ console.log(paint('Push follow-ups (skip if you only use OTA)', C.bold + C.yellow));
704
+ todo.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
705
+ }
706
+ }
707
+
708
+ function runInitAndroid(argv) {
709
+ const flags = new Set(argv);
710
+ const isCheck = flags.has('--check');
711
+ const isDryRun = flags.has('--dry-run');
712
+
713
+ const androidDir = findAndroidDir();
714
+ if (!androidDir) {
715
+ console.error(paint('✗ No android/ directory found.', C.red));
716
+ console.error(' Run this from a React Native project root (the folder containing android/).');
717
+ process.exit(2);
718
+ }
719
+
720
+ const found = androidCodemod.findMainApplication(androidDir, fs, path);
721
+ if (!found) {
722
+ console.error(paint('✗ Could not find MainApplication.kt or .java under android/app/src/main.', C.red));
723
+ console.error(' This project uses a non-standard layout — wire OTA bundle resolution manually.');
724
+ process.exit(2);
725
+ }
726
+
727
+ const minor = rnMinor();
728
+ const rel = path.relative(CWD, found.path);
729
+ console.log('');
730
+ console.log(paint('ScaleBun init android', C.bold + C.cyan));
731
+ console.log(` MainApplication: ${paint(rel, C.cyan)} (${found.kind})`);
732
+
733
+ // Java: not auto-patched — print clear manual instructions, fail non-zero.
734
+ if (found.kind === 'java') {
735
+ console.log('');
736
+ console.log(androidCodemod.javaInstructions(minor));
737
+ printAndroidChecklist(detectAndroidCapabilities(androidDir));
738
+ process.exit(isCheck ? 1 : 2);
739
+ }
740
+
741
+ const original = readText(found.path) || '';
742
+
743
+ // --check: verify, never write.
744
+ if (isCheck) {
745
+ const a = androidCodemod.analyzeKotlin(original);
746
+ const wired = androidCodemod.isFullyWiredKotlin(original, minor);
747
+ console.log('');
748
+ console.log(paint('OTA wiring status', C.bold));
749
+ line(a.hasImport ? OK() : FAIL(), 'ScaleBunOtaModule import', a.hasImport ? 'present' : null);
750
+ line(a.hasBundleWiring ? OK() : FAIL(), 'getJSBundleFile wiring', a.hasBundleWiring ? 'present' : null);
751
+ if (androidCodemod.isReactHostEra(minor)) {
752
+ line(a.hasJsBundleFilePathArg ? OK() : FAIL(), 'jsBundleFilePath arg (RN >= 0.82)', a.hasJsBundleFilePathArg ? 'present' : null);
753
+ }
754
+ printAndroidChecklist(detectAndroidCapabilities(androidDir));
755
+ console.log('');
756
+ if (wired) {
757
+ console.log(paint('✓ MainApplication is wired for ScaleBun OTA bundle resolution.', C.green));
758
+ process.exit(0);
759
+ }
760
+ console.log(paint('✗ MainApplication is missing OTA bundle wiring. Run `npx scalebun init android`.', C.red));
761
+ process.exit(1);
762
+ }
763
+
764
+ const result = androidCodemod.patchKotlin(original, { minor });
765
+
766
+ if (!result.changed) {
767
+ console.log('');
768
+ if (result.manual.length) {
769
+ console.log(paint('Could not auto-apply — wire it by hand:', C.bold + C.yellow));
770
+ result.manual.forEach((m) => console.log(m));
771
+ } else {
772
+ console.log(paint('✓ Already wired — no changes needed (idempotent).', C.green));
773
+ }
774
+ printAndroidChecklist(detectAndroidCapabilities(androidDir));
775
+ process.exit(result.manual.length ? 1 : 0);
776
+ }
777
+
778
+ console.log('');
779
+ console.log(paint(isDryRun ? 'Planned changes (--dry-run, nothing written)' : 'Changes', C.bold));
780
+ result.summary.forEach((sline, i) => console.log(` ${i + 1}. ${sline}`));
781
+ if (result.manual.length) {
782
+ console.log('');
783
+ console.log(paint('Could not auto-apply (edit by hand)', C.bold + C.yellow));
784
+ result.manual.forEach((m) => console.log(m));
785
+ }
786
+
787
+ if (isDryRun) {
788
+ console.log('');
789
+ console.log(paint('Dry run — no files written. Re-run without --dry-run to apply.', C.dim));
790
+ printAndroidChecklist(detectAndroidCapabilities(androidDir));
791
+ process.exit(0);
792
+ }
793
+
794
+ // Apply. Never stage / commit — only edits the working-tree file.
795
+ fs.writeFileSync(found.path, result.source, 'utf8');
796
+ console.log('');
797
+ console.log(paint(`✓ Patched ${rel}`, C.green));
798
+ console.log(paint(' (working-tree edit only — nothing staged or committed)', C.dim));
799
+
800
+ printAndroidChecklist(detectAndroidCapabilities(androidDir));
801
+
802
+ console.log('');
803
+ console.log(paint('Next:', C.bold));
804
+ console.log(' Rebuild the native app (a JS reload does not load the newly-wired native path):');
805
+ console.log(paint(' npx react-native run-android # or your gradle build', C.dim));
806
+ console.log('');
807
+ process.exit(0);
808
+ }
809
+
600
810
  // ─── entry ──────────────────────────────────────────────────────────────────
601
811
 
602
812
  function main() {
@@ -610,9 +820,11 @@ function main() {
610
820
  const target = argv[1];
611
821
  if (target === 'ios') {
612
822
  runInitIos(argv.slice(2));
823
+ } else if (target === 'android') {
824
+ runInitAndroid(argv.slice(2));
613
825
  } else {
614
826
  console.error(`Unknown init target: ${target ?? '(none)'}`);
615
- console.error('Usage: npx scalebun init ios [--check | --dry-run]');
827
+ console.error('Usage: npx scalebun init <ios|android> [--check | --dry-run]');
616
828
  process.exit(2);
617
829
  }
618
830
  break;
@@ -621,21 +833,101 @@ function main() {
621
833
  case 'help':
622
834
  case '--help':
623
835
  case '-h':
624
- console.log('');
625
- console.log(paint('ScaleBun SDK CLI', C.bold));
626
- console.log('');
627
- console.log('Usage:');
628
- console.log(' npx scalebun doctor Diagnose push-notification setup');
629
- console.log(' npx scalebun init ios Wire the iOS AppDelegate for Direct APNs');
630
- console.log(' npx scalebun init ios --check Verify hooks exist (exit 1 if missing)');
631
- console.log(' npx scalebun init ios --dry-run Preview changes without writing');
632
- console.log('');
836
+ printFullHelp();
633
837
  break;
634
838
  default:
635
- console.error(`Unknown command: ${cmd}`);
636
- console.error('Run `npx scalebun help` for usage.');
637
- process.exit(2);
839
+ // TWO PACKAGES, ONE COMMAND NAME.
840
+ //
841
+ // The OTA commands (login, ota publish, releases, rollout, quickstart…)
842
+ // live in @scalebun/cli, which declares the SAME `scalebun` bin as this
843
+ // package. Every doc and generated CI pipeline says `npx scalebun ota
844
+ // publish`, so whichever package won the bin race decided whether that
845
+ // worked — and when this one won, a new user's very first command died
846
+ // with "Unknown command: ota" and no way to find out why.
847
+ //
848
+ // Delegate instead of guessing: if @scalebun/cli is installed, run it.
849
+ // If not, say exactly what to install rather than what went wrong.
850
+ delegateToOtaCli(argv);
638
851
  }
639
852
  }
640
853
 
854
+ /** Resolve the bundled @scalebun/cli compiled entry, or null when absent. */
855
+ function resolveOtaCli() {
856
+ try {
857
+ return require.resolve('@scalebun/cli/lib/index.js', {
858
+ paths: [process.cwd(), __dirname],
859
+ });
860
+ } catch {
861
+ return null;
862
+ }
863
+ }
864
+
865
+ /**
866
+ * Hand the invocation to @scalebun/cli, preserving argv and exit code.
867
+ * Falls back to an actionable install instruction when it is not present.
868
+ */
869
+ function delegateToOtaCli(argv) {
870
+ const cliEntry = resolveOtaCli();
871
+
872
+ if (!cliEntry) {
873
+ const cmd = argv[0];
874
+ console.error('');
875
+ console.error(`"${cmd}" is an OTA command, provided by @scalebun/cli.`);
876
+ console.error('');
877
+ console.error('@scalebun/cli ships as a dependency of @scalebun/react-native, so it');
878
+ console.error('should already be present. It could not be resolved — your install may');
879
+ console.error('be incomplete or the dependency was pruned.');
880
+ console.error('');
881
+ console.error('Reinstall dependencies, then re-run:');
882
+ console.error(' npm install');
883
+ console.error(` npx scalebun ${argv.join(' ')}`);
884
+ console.error('');
885
+ console.error('(Or install it explicitly: npm i -D @scalebun/cli)');
886
+ console.error('');
887
+ process.exit(2);
888
+ }
889
+
890
+ const { spawnSync } = require('child_process');
891
+ const res = spawnSync(process.execPath, [cliEntry, ...argv], {
892
+ stdio: 'inherit',
893
+ });
894
+ process.exit(res.status ?? 1);
895
+ }
896
+
897
+ /**
898
+ * Print the full command surface: the SDK-setup commands this package handles
899
+ * directly, followed by the complete @scalebun/cli command tree.
900
+ *
901
+ * The CLI tree is rendered by delegating to `@scalebun/cli --help` rather than a
902
+ * hardcoded list, so it is a single source of truth and cannot drift. When the
903
+ * CLI cannot be resolved (should not happen — it is a dependency) we fall back to
904
+ * a prose note instead of an error, since this is a help screen.
905
+ */
906
+ function printFullHelp() {
907
+ console.log('');
908
+ console.log(paint('ScaleBun SDK CLI', C.bold));
909
+ console.log('');
910
+ console.log(paint('SDK setup (bundled with @scalebun/react-native):', C.bold));
911
+ console.log(' npx scalebun doctor Diagnose push-notification setup');
912
+ console.log(' npx scalebun init ios Wire the iOS AppDelegate for Direct APNs');
913
+ console.log(' npx scalebun init android Wire Android MainApplication for OTA bundle loading');
914
+ console.log(paint(' add --check to verify (exit 1 if missing) or --dry-run to preview', C.dim));
915
+ console.log('');
916
+
917
+ const cliEntry = resolveOtaCli();
918
+ if (!cliEntry) {
919
+ console.log(paint('OTA & account commands:', C.bold));
920
+ console.log(' Provided by @scalebun/cli (login, quickstart, apps, ota publish,');
921
+ console.log(' releases, rollout…). It ships with this package but could not be');
922
+ console.log(' resolved — run `npm install`, then `npx scalebun --help` again.');
923
+ console.log('');
924
+ return;
925
+ }
926
+
927
+ console.log(paint('OTA & account commands (from @scalebun/cli):', C.bold));
928
+ const { spawnSync } = require('child_process');
929
+ spawnSync(process.execPath, [cliEntry, '--help'], { stdio: 'inherit' });
930
+ console.log('');
931
+ }
932
+
641
933
  main();
@@ -489,7 +489,7 @@ var SDK_VERSION;
489
489
  var init_version = __esm({
490
490
  "lib/module/core/constants/version.js"() {
491
491
  "use strict";
492
- SDK_VERSION = "1.4.0";
492
+ SDK_VERSION = "1.6.0";
493
493
  }
494
494
  });
495
495
 
@@ -10041,6 +10041,10 @@ function enableDebug(config) {
10041
10041
  __DEV__ && logger.debug("Debug already enabled, skipping");
10042
10042
  return;
10043
10043
  }
10044
+ if (!__DEV__ && !config.allowInRelease) {
10045
+ logger.warn("[ScaleBun] Debug connection refused in a release build. The debug transport is unencrypted (ws://) and carries captured traffic and the shared secret in cleartext, so it is development-only by default. If you genuinely need it in a release build, set `allowInRelease: true` \u2014 and do not ship that to users.");
10046
+ return;
10047
+ }
10044
10048
  if (!isDevToolsBundled()) {
10045
10049
  logger.warn("[ScaleBun] debugConnection was configured, but the desktop-debugger tooling is not in this bundle. It is excluded by default (the ScaleBun desktop app is not yet released); opt in with `withScaleBun(config, { features: { devTools: true } })` in metro.config.js. All other SDK behaviour is unaffected.");
10046
10050
  return;
@@ -10550,89 +10554,6 @@ var init_nativeCrashBridge = __esm({
10550
10554
  }
10551
10555
  });
10552
10556
 
10553
- // lib/module/analytics/batching.js
10554
- function chunkEvents(events, size = MAX_BATCH_EVENTS) {
10555
- const cap = Math.max(1, Math.min(size, MAX_BATCH_EVENTS));
10556
- if (events.length <= cap) return events.length ? [events.slice()] : [];
10557
- const out = [];
10558
- for (let i = 0; i < events.length; i += cap) {
10559
- out.push(events.slice(i, i + cap));
10560
- }
10561
- return out;
10562
- }
10563
- function clampBatchSize(configured, fallback) {
10564
- const n = typeof configured === "number" && configured > 0 ? configured : fallback;
10565
- return Math.max(1, Math.min(n, MAX_BATCH_EVENTS));
10566
- }
10567
- function batchIdempotencyKey(eventIds) {
10568
- let h = 2166136261;
10569
- const joined = eventIds.join("");
10570
- for (let i = 0; i < joined.length; i++) {
10571
- h ^= joined.charCodeAt(i);
10572
- h = Math.imul(h, 16777619) >>> 0;
10573
- }
10574
- return `sdk-${eventIds.length}-${h.toString(16).padStart(8, "0")}`;
10575
- }
10576
- var MAX_BATCH_EVENTS;
10577
- var init_batching = __esm({
10578
- "lib/module/analytics/batching.js"() {
10579
- "use strict";
10580
- MAX_BATCH_EVENTS = 500;
10581
- }
10582
- });
10583
-
10584
- // lib/module/analytics/automaticEvents.js
10585
- function compactProperties(properties) {
10586
- const out = {
10587
- capture_source: "automatic"
10588
- };
10589
- for (const [key, value] of Object.entries(properties ?? {})) {
10590
- if (value !== void 0) out[key] = value;
10591
- }
10592
- return out;
10593
- }
10594
- function emitAutomaticEvent(name, properties) {
10595
- const event = {
10596
- name,
10597
- properties: compactProperties(properties)
10598
- };
10599
- if (listeners.size === 0) {
10600
- pending3.push(event);
10601
- if (pending3.length > MAX_PENDING2) pending3.shift();
10602
- return;
10603
- }
10604
- for (const listener of listeners) {
10605
- try {
10606
- listener(event);
10607
- } catch {
10608
- }
10609
- }
10610
- }
10611
- function subscribeAutomaticEvents(listener) {
10612
- listeners.add(listener);
10613
- if (pending3.length > 0) {
10614
- const buffered = pending3.splice(0, pending3.length);
10615
- for (const event of buffered) {
10616
- try {
10617
- listener(event);
10618
- } catch {
10619
- }
10620
- }
10621
- }
10622
- return () => {
10623
- listeners.delete(listener);
10624
- };
10625
- }
10626
- var listeners, pending3, MAX_PENDING2;
10627
- var init_automaticEvents = __esm({
10628
- "lib/module/analytics/automaticEvents.js"() {
10629
- "use strict";
10630
- listeners = /* @__PURE__ */ new Set();
10631
- pending3 = [];
10632
- MAX_PENDING2 = 50;
10633
- }
10634
- });
10635
-
10636
10557
  // lib/module/crypto/hmacSha256.js
10637
10558
  function utf8(str3) {
10638
10559
  const out = [];
@@ -10749,6 +10670,89 @@ var init_hmacSha256 = __esm({
10749
10670
  }
10750
10671
  });
10751
10672
 
10673
+ // lib/module/analytics/batching.js
10674
+ function chunkEvents(events, size = MAX_BATCH_EVENTS) {
10675
+ const cap = Math.max(1, Math.min(size, MAX_BATCH_EVENTS));
10676
+ if (events.length <= cap) return events.length ? [events.slice()] : [];
10677
+ const out = [];
10678
+ for (let i = 0; i < events.length; i += cap) {
10679
+ out.push(events.slice(i, i + cap));
10680
+ }
10681
+ return out;
10682
+ }
10683
+ function clampBatchSize(configured, fallback) {
10684
+ const n = typeof configured === "number" && configured > 0 ? configured : fallback;
10685
+ return Math.max(1, Math.min(n, MAX_BATCH_EVENTS));
10686
+ }
10687
+ function batchIdempotencyKey(eventIds) {
10688
+ let h = 2166136261;
10689
+ const joined = eventIds.join("");
10690
+ for (let i = 0; i < joined.length; i++) {
10691
+ h ^= joined.charCodeAt(i);
10692
+ h = Math.imul(h, 16777619) >>> 0;
10693
+ }
10694
+ return `sdk-${eventIds.length}-${h.toString(16).padStart(8, "0")}`;
10695
+ }
10696
+ var MAX_BATCH_EVENTS;
10697
+ var init_batching = __esm({
10698
+ "lib/module/analytics/batching.js"() {
10699
+ "use strict";
10700
+ MAX_BATCH_EVENTS = 500;
10701
+ }
10702
+ });
10703
+
10704
+ // lib/module/analytics/automaticEvents.js
10705
+ function compactProperties(properties) {
10706
+ const out = {
10707
+ capture_source: "automatic"
10708
+ };
10709
+ for (const [key, value] of Object.entries(properties ?? {})) {
10710
+ if (value !== void 0) out[key] = value;
10711
+ }
10712
+ return out;
10713
+ }
10714
+ function emitAutomaticEvent(name, properties) {
10715
+ const event = {
10716
+ name,
10717
+ properties: compactProperties(properties)
10718
+ };
10719
+ if (listeners.size === 0) {
10720
+ pending3.push(event);
10721
+ if (pending3.length > MAX_PENDING2) pending3.shift();
10722
+ return;
10723
+ }
10724
+ for (const listener of listeners) {
10725
+ try {
10726
+ listener(event);
10727
+ } catch {
10728
+ }
10729
+ }
10730
+ }
10731
+ function subscribeAutomaticEvents(listener) {
10732
+ listeners.add(listener);
10733
+ if (pending3.length > 0) {
10734
+ const buffered = pending3.splice(0, pending3.length);
10735
+ for (const event of buffered) {
10736
+ try {
10737
+ listener(event);
10738
+ } catch {
10739
+ }
10740
+ }
10741
+ }
10742
+ return () => {
10743
+ listeners.delete(listener);
10744
+ };
10745
+ }
10746
+ var listeners, pending3, MAX_PENDING2;
10747
+ var init_automaticEvents = __esm({
10748
+ "lib/module/analytics/automaticEvents.js"() {
10749
+ "use strict";
10750
+ listeners = /* @__PURE__ */ new Set();
10751
+ pending3 = [];
10752
+ MAX_PENDING2 = 50;
10753
+ }
10754
+ });
10755
+
10752
10756
  // lib/module/analytics/EventTracker.js
10753
10757
  function uuid() {
10754
10758
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
@@ -11114,6 +11118,38 @@ var init_EventTracker = __esm({
11114
11118
  }
11115
11119
  });
11116
11120
 
11121
+ // lib/module/core/id/installationId.js
11122
+ function resolveInstallationId() {
11123
+ try {
11124
+ const storage3 = createStorageBackend();
11125
+ const existing = storage3.get(K_INSTALLATION_ID);
11126
+ if (existing) return existing;
11127
+ const id = `inst-${randomSegment()}${randomSegment()}`;
11128
+ storage3.set(K_INSTALLATION_ID, id);
11129
+ return id;
11130
+ } catch {
11131
+ return `inst-ephemeral-${randomSegment()}`;
11132
+ }
11133
+ }
11134
+ function randomSegment() {
11135
+ try {
11136
+ const g = globalThis;
11137
+ if (g.crypto?.getRandomValues) {
11138
+ const bytes = g.crypto.getRandomValues(new Uint8Array(6));
11139
+ return Array.from(bytes).map((b) => b.toString(36).padStart(2, "0")).join("").slice(0, 8);
11140
+ }
11141
+ } catch {
11142
+ }
11143
+ return Math.random().toString(36).slice(2, 10).padEnd(8, "0");
11144
+ }
11145
+ var init_installationId = __esm({
11146
+ "lib/module/core/id/installationId.js"() {
11147
+ "use strict";
11148
+ init_StorageBackend();
11149
+ init_EventTracker();
11150
+ }
11151
+ });
11152
+
11117
11153
  // lib/module/features/engage/engageSignals.js
11118
11154
  var EngageSignalBus, engageSignals;
11119
11155
  var init_engageSignals = __esm({
@@ -11573,18 +11609,6 @@ function subscribeNativeProgress(onTick) {
11573
11609
  };
11574
11610
  }
11575
11611
  }
11576
- function resolveInstallationId() {
11577
- try {
11578
- const storage3 = createStorageBackend();
11579
- const existing = storage3.get(K_INSTALLATION_ID);
11580
- if (existing) return existing;
11581
- const id = `inst-${Math.random().toString(36).slice(2, 10)}${Math.random().toString(36).slice(2, 10)}`;
11582
- storage3.set(K_INSTALLATION_ID, id);
11583
- return id;
11584
- } catch {
11585
- return `inst-ephemeral-${Math.random().toString(36).slice(2, 10)}`;
11586
- }
11587
- }
11588
11612
  async function deliverOtaEvents(params) {
11589
11613
  const events = otaEventEmitter.flush();
11590
11614
  if (!events.length) return;
@@ -11634,7 +11658,7 @@ var init_OtaOrchestrator = __esm({
11634
11658
  init_OtaEventEmitter();
11635
11659
  init_geoCountry();
11636
11660
  init_deviceAttributes();
11637
- init_EventTracker();
11661
+ init_installationId();
11638
11662
  init_signature();
11639
11663
  init_environment();
11640
11664
  init_retry();
@@ -15374,6 +15398,7 @@ function decodeManual(base64) {
15374
15398
  }
15375
15399
 
15376
15400
  // lib/module/features/session/BackendSessionAdapter.js
15401
+ init_installationId();
15377
15402
  init_version();
15378
15403
  init_bridgeAdapter();
15379
15404
  init_nativeModule();
@@ -15509,6 +15534,13 @@ var BackendSessionAdapter = class _BackendSessionAdapter {
15509
15534
  const payload = {
15510
15535
  sessionId: session.sessionId,
15511
15536
  deviceId: session.deviceId,
15537
+ // The canonical per-install id — the SAME value OTA events are
15538
+ // reported under, and a DIFFERENT value from deviceId. The
15539
+ // backend stores it on the Device so release health can join a
15540
+ // bundle's installs to that device's sessions. Without it, crash
15541
+ // impact for a release resolves to "no data" for every device
15542
+ // that never registered for push.
15543
+ installationId: resolveInstallationId(),
15512
15544
  startedAt: session.startedAt,
15513
15545
  // Link the recording row to the always-on analytics row so the
15514
15546
  // dashboard can join them. track() events land on the analytics