@scalebun/react-native 1.13.0 → 2.0.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 (115) hide show
  1. package/android/build.gradle +8 -0
  2. package/android/src/androidTest/java/com/scalebun/rn/ota/ScaleBunOtaVerifierInstrumentedTest.kt +112 -0
  3. package/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +10 -0
  4. package/android/src/main/java/com/scalebun/rn/ota/DeviceIntegrity.kt +75 -0
  5. package/android/src/main/java/com/scalebun/rn/ota/OtaProtocol.kt +244 -0
  6. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaKeyRegistry.kt +100 -0
  7. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +132 -62
  8. package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaReleaseVerifier.kt +129 -0
  9. package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +80 -1
  10. package/android/src/main/java/com/scalebun/rn/ota/TlsPinning.kt +118 -0
  11. package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +6 -8
  12. package/dist/scalebun.full.js +964 -533
  13. package/dist/scalebun.slim.js +962 -531
  14. package/ios/Ota/OtaProtocol.swift +242 -0
  15. package/ios/Ota/OtaSlotManager.swift +26 -0
  16. package/ios/Ota/ScaleBunOtaBridge.mm +6 -3
  17. package/ios/Ota/ScaleBunOtaKeyRegistry.swift +147 -0
  18. package/ios/Ota/ScaleBunOtaModule.swift +125 -53
  19. package/ios/Ota/ScaleBunOtaReleaseVerifier.swift +115 -0
  20. package/ios/Ota/ScaleBunOtaVerifierTests.swift +92 -0
  21. package/ios/ReplaySdk.swift +65 -2
  22. package/lib/commonjs/bootstrap/SDKBootstrapper.js +30 -0
  23. package/lib/commonjs/core/config/schema.js +19 -12
  24. package/lib/commonjs/core/constants/version.js +1 -1
  25. package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +293 -5
  26. package/lib/commonjs/features/journey/ScaleBunScrollView.js +28 -0
  27. package/lib/commonjs/features/journey/autoInstrumentScroll.js +161 -0
  28. package/lib/commonjs/features/journey/interactionProtocol.js +131 -10
  29. package/lib/commonjs/features/journey/scrollContext.js +146 -0
  30. package/lib/commonjs/features/journey/targetGeometry.js +164 -0
  31. package/lib/commonjs/features/journey/touchTarget.js +152 -24
  32. package/lib/commonjs/features/journey/uiState.js +81 -1
  33. package/lib/commonjs/features/navigation/AutoScreenDetector.js +74 -5
  34. package/lib/commonjs/features/network/NetworkFeature.js +39 -5
  35. package/lib/commonjs/features/network/thirdParty.js +90 -0
  36. package/lib/commonjs/features/ota/OtaOrchestrator.js +175 -80
  37. package/lib/commonjs/features/session/SessionManager.js +63 -0
  38. package/lib/commonjs/public/ScaleBunFacade.js +23 -57
  39. package/lib/module/bootstrap/SDKBootstrapper.js +30 -0
  40. package/lib/module/core/config/schema.js +19 -12
  41. package/lib/module/core/constants/version.js +1 -1
  42. package/lib/module/features/journey/ScaleBunDebugRoot.js +295 -7
  43. package/lib/module/features/journey/ScaleBunScrollView.js +28 -0
  44. package/lib/module/features/journey/autoInstrumentScroll.js +155 -0
  45. package/lib/module/features/journey/interactionProtocol.js +128 -8
  46. package/lib/module/features/journey/scrollContext.js +135 -0
  47. package/lib/module/features/journey/targetGeometry.js +155 -0
  48. package/lib/module/features/journey/touchTarget.js +151 -24
  49. package/lib/module/features/journey/uiState.js +78 -1
  50. package/lib/module/features/navigation/AutoScreenDetector.js +74 -5
  51. package/lib/module/features/network/NetworkFeature.js +38 -4
  52. package/lib/module/features/network/thirdParty.js +83 -0
  53. package/lib/module/features/ota/OtaOrchestrator.js +175 -80
  54. package/lib/module/features/session/SessionManager.js +63 -0
  55. package/lib/module/public/ScaleBunFacade.js +23 -57
  56. package/lib/typescript/core/config/schema.d.ts +2 -3
  57. package/lib/typescript/core/constants/version.d.ts +1 -1
  58. package/lib/typescript/features/journey/autoInstrumentScroll.d.ts +46 -0
  59. package/lib/typescript/features/journey/interactionProtocol.d.ts +139 -1
  60. package/lib/typescript/features/journey/scrollContext.d.ts +85 -0
  61. package/lib/typescript/features/journey/targetGeometry.d.ts +114 -0
  62. package/lib/typescript/features/journey/touchTarget.d.ts +148 -15
  63. package/lib/typescript/features/journey/uiState.d.ts +39 -0
  64. package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +48 -2
  65. package/lib/typescript/features/network/index.d.ts +18 -0
  66. package/lib/typescript/features/network/thirdParty.d.ts +65 -0
  67. package/lib/typescript/features/ota/OtaOrchestrator.d.ts +12 -8
  68. package/lib/typescript/features/ota/OtaTypes.d.ts +22 -0
  69. package/lib/typescript/features/session/SessionManager.d.ts +55 -0
  70. package/lib/typescript/public/ScaleBunFacade.d.ts +5 -11
  71. package/lib/typescript/public/types.d.ts +10 -0
  72. package/lib/typescript/specs/NativeScaleBunOta.d.ts +32 -20
  73. package/package.json +3 -13
  74. package/scalebun-react-native.podspec +4 -0
  75. package/src/bootstrap/SDKBootstrapper.ts +40 -0
  76. package/src/core/config/schema.ts +21 -9
  77. package/src/core/constants/version.ts +1 -1
  78. package/src/features/journey/ScaleBunDebugRoot.tsx +288 -7
  79. package/src/features/journey/ScaleBunScrollView.tsx +29 -0
  80. package/src/features/journey/autoInstrumentScroll.ts +155 -0
  81. package/src/features/journey/interactionProtocol.ts +188 -10
  82. package/src/features/journey/scrollContext.ts +137 -0
  83. package/src/features/journey/targetGeometry.ts +176 -0
  84. package/src/features/journey/touchTarget.ts +237 -26
  85. package/src/features/journey/uiState.ts +80 -1
  86. package/src/features/navigation/AutoScreenDetector.ts +65 -5
  87. package/src/features/network/NetworkFeature.ts +41 -4
  88. package/src/features/network/index.ts +18 -0
  89. package/src/features/network/thirdParty.ts +92 -0
  90. package/src/features/ota/OtaOrchestrator.ts +190 -90
  91. package/src/features/ota/OtaTypes.ts +23 -1
  92. package/src/features/session/SessionManager.ts +113 -0
  93. package/src/public/ScaleBunFacade.ts +23 -69
  94. package/src/public/types.ts +10 -0
  95. package/src/specs/NativeScaleBunOta.ts +35 -20
  96. package/lib/commonjs/features/ota/crypto/builtinVerifier.js +0 -248
  97. package/lib/commonjs/features/ota/crypto/loadEd25519.js +0 -40
  98. package/lib/commonjs/features/ota/crypto/loadSha512.js +0 -40
  99. package/lib/commonjs/features/ota/crypto/nativeVerifier.js +0 -121
  100. package/lib/commonjs/features/ota/signature.js +0 -175
  101. package/lib/module/features/ota/crypto/builtinVerifier.js +0 -240
  102. package/lib/module/features/ota/crypto/loadEd25519.js +0 -34
  103. package/lib/module/features/ota/crypto/loadSha512.js +0 -34
  104. package/lib/module/features/ota/crypto/nativeVerifier.js +0 -113
  105. package/lib/module/features/ota/signature.js +0 -169
  106. package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +0 -53
  107. package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +0 -30
  108. package/lib/typescript/features/ota/crypto/loadSha512.d.ts +0 -15
  109. package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +0 -35
  110. package/lib/typescript/features/ota/signature.d.ts +0 -81
  111. package/src/features/ota/crypto/builtinVerifier.ts +0 -257
  112. package/src/features/ota/crypto/loadEd25519.ts +0 -41
  113. package/src/features/ota/crypto/loadSha512.ts +0 -35
  114. package/src/features/ota/crypto/nativeVerifier.ts +0 -117
  115. package/src/features/ota/signature.ts +0 -206
@@ -49,7 +49,10 @@ const clean = s => typeof s === 'string' ? s.replace(/[;:|]/g, '').trim().slice(
49
49
  export function setUiState(name, value) {
50
50
  const n = clean(name);
51
51
  const v = clean(value);
52
- if (n && v) declared.set(n, v);
52
+ if (!n || !v) return;
53
+ if (declared.get(n) === v) return;
54
+ declared.set(n, v);
55
+ notifyChanged();
53
56
  }
54
57
 
55
58
  /**
@@ -61,7 +64,9 @@ export function setUiState(name, value) {
61
64
  * screen.
62
65
  */
63
66
  export function clearUiState(name) {
67
+ const before = declared.size;
64
68
  if (name === undefined) declared.clear();else declared.delete(clean(name));
69
+ if (declared.size !== before) notifyChanged();
65
70
  }
66
71
 
67
72
  /**
@@ -83,4 +88,76 @@ export function uiStateSignature() {
83
88
  }
84
89
  return signature || undefined;
85
90
  }
91
+
92
+ /**
93
+ * RN-11 — TELL SOMEONE THE STATE CHANGED, so a frame can exist for it.
94
+ *
95
+ * THE GAP. Declaring a state used to mutate a Map and nothing else. Nothing captured a frame, so
96
+ * the visual for a declared state existed only if a screenshot happened to be taken for some other
97
+ * reason — a navigation, a timer, or the interaction capture that fires AFTER the touch and
98
+ * photographs the outcome. A state could therefore be recorded as a fact with no picture of it
99
+ * anywhere, or with a picture of the state that replaced it.
100
+ *
101
+ * The native capture reasons are NAVIGATION, MANUAL, INTERACTION and TIMER. None of them is "the
102
+ * UI changed shape", which is exactly the moment a heatmap needs a frame.
103
+ *
104
+ * FIRING ON A REAL CHANGE ONLY, AND WHY THAT IS THE SDK'S JOB. The advice above says to call
105
+ * `setUiState` when the state changes rather than on every render — but that is advice to the
106
+ * host, not a guarantee. A component that declares its state inside render is ordinary React, and
107
+ * trusting the caller would mean a screenshot per render: a performance disaster in exactly the
108
+ * apps that instrument most carefully. So a redundant set — same dimension, same value — is now a
109
+ * no-op, and the change test lives here where it cannot be got wrong by a host.
110
+ *
111
+ * COALESCED, because a transition usually declares several dimensions in one tick ("drawer:open"
112
+ * and "overlay:visible" belong to one visual change). Notifying per call would capture the
113
+ * intermediate shapes, which are states the user never saw.
114
+ *
115
+ * The listener is deliberately NOT given the signature. A subscriber that wants it calls
116
+ * `uiStateSignature()` at the moment it acts, which is the value that will actually be attached
117
+ * to the next tap; handing over a snapshot invites acting on a state that has already moved on.
118
+ */
119
+
120
+ const listeners = new Set();
121
+ let pending = null;
122
+
123
+ /**
124
+ * Subscribe to declared-state changes. Returns an unsubscribe.
125
+ *
126
+ * Registration is idempotent per function reference, so a component that re-subscribes on every
127
+ * render does not accumulate listeners — a leak that would only show up in long sessions.
128
+ */
129
+ export function onUiStateChange(listener) {
130
+ listeners.add(listener);
131
+ return () => {
132
+ listeners.delete(listener);
133
+ };
134
+ }
135
+
136
+ /** Drop every listener and any pending notification. For teardown between sessions. */
137
+ export function resetUiStateListeners() {
138
+ listeners.clear();
139
+ if (pending !== null) {
140
+ clearTimeout(pending);
141
+ pending = null;
142
+ }
143
+ }
144
+
145
+ /** For assertions that a re-subscribing caller has not leaked. */
146
+ export function uiStateListenerCount() {
147
+ return listeners.size;
148
+ }
149
+ function notifyChanged() {
150
+ if (pending !== null) return;
151
+ pending = setTimeout(() => {
152
+ pending = null;
153
+ for (const listener of listeners) {
154
+ try {
155
+ listener();
156
+ } catch {
157
+ /* One bad subscriber must not stop the others, and must never surface to the host: a
158
+ screenshot is not worth an app crash. */
159
+ }
160
+ }
161
+ }, 0);
162
+ }
86
163
  //# sourceMappingURL=uiState.js.map
@@ -36,6 +36,15 @@ export class AutoScreenDetector {
36
36
  _manualScreen = null;
37
37
  _manualScreenTs = 0;
38
38
  _autoScreen = null;
39
+ /**
40
+ * When `_autoScreen` was last written.
41
+ *
42
+ * Needed so `getCurrentScreen()` can prefer whichever source spoke MOST RECENTLY.
43
+ * Without a timestamp the only comparison available was "is the manual value still
44
+ * fresh", which is a different question and gave the wrong answer — see the note on
45
+ * `_currentScreen()`.
46
+ */
47
+ _autoScreenTs = 0;
39
48
  /** `name#key` of the last auto-detected screen VISIT — see _extractActiveVisit. */
40
49
  _autoVisitSig = null;
41
50
  _navigationRef = null;
@@ -110,6 +119,7 @@ export class AutoScreenDetector {
110
119
  if (this.isManualScreenActive()) return;
111
120
  if (name !== this._autoScreen) {
112
121
  this._autoScreen = name;
122
+ this._autoScreenTs = Date.now();
113
123
  this._autoVisitSig = name; // native lifecycle carries no route key — name-only visit
114
124
  this._emitScreenChange(name, 'heuristic');
115
125
  }
@@ -117,8 +127,19 @@ export class AutoScreenDetector {
117
127
  }
118
128
 
119
129
  /**
120
- * Check if a manual screen is currently active.
121
- * Used by auto-detection to yield to manual overrides.
130
+ * Should a WRITER defer to the manual screen right now?
131
+ *
132
+ * THIS IS A PRECEDENCE GATE, NOT A VALIDITY TEST, and conflating the two was a real
133
+ * bug. Its job is what the cooldown constant says: for a short window after a manual
134
+ * beacon fires, an auto source must not overwrite it — the poller runs every 1500 ms
135
+ * and would otherwise clobber a fresh declaration with whatever it inferred.
136
+ *
137
+ * `getCurrentScreen()` used to reuse this as "is the manual screen still the current
138
+ * screen", which made a declared screen READABLE for only 2000 ms. Nothing called
139
+ * `setManualScreen` in production, so the effect was latent — but the moment the
140
+ * `<ScaleBunScreen>` beacon was wired to it (RN-8), every tap more than two seconds
141
+ * after a screen mounted would have lost its name, which is very nearly every tap. A
142
+ * fix that introduced that would have been worse than the gap it closed.
122
143
  */
123
144
  isManualScreenActive() {
124
145
  if (!this._manualScreen) return false;
@@ -178,6 +199,7 @@ export class AutoScreenDetector {
178
199
  const visit = this._extractActiveVisit(state);
179
200
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
180
201
  this._autoScreen = visit.name;
202
+ this._autoScreenTs = Date.now();
181
203
  this._autoVisitSig = this._visitSig(visit);
182
204
  this._emitScreenChange(visit.name, 'navRef');
183
205
  }
@@ -196,6 +218,7 @@ export class AutoScreenDetector {
196
218
  const visit = this._extractActiveVisit(state);
197
219
  if (visit) {
198
220
  this._autoScreen = visit.name;
221
+ this._autoScreenTs = Date.now();
199
222
  this._autoVisitSig = this._visitSig(visit);
200
223
  this._emitScreenChange(visit.name, 'navRef');
201
224
  }
@@ -239,6 +262,7 @@ export class AutoScreenDetector {
239
262
  const visit = this._extractActiveVisit(state);
240
263
  if (visit && this._visitSig(visit) !== this._autoVisitSig) {
241
264
  this._autoScreen = visit.name;
265
+ this._autoScreenTs = Date.now();
242
266
  this._autoVisitSig = this._visitSig(visit);
243
267
  this._emitScreenChange(visit.name, 'heuristic');
244
268
  }
@@ -495,16 +519,61 @@ export class AutoScreenDetector {
495
519
  this._listeners = [];
496
520
  this._manualScreen = null;
497
521
  this._autoScreen = null;
522
+ this._autoScreenTs = 0;
498
523
  this._navigationRef = null;
499
524
  _instance = null;
500
525
  }
501
526
 
502
527
  /** Get the current best-known screen name */
503
528
  getCurrentScreen() {
504
- if (this._manualScreen && this.isManualScreenActive()) {
505
- return this._manualScreen;
529
+ return this._currentScreen().name;
530
+ }
531
+
532
+ /**
533
+ * WHERE the current screen name came from, or 'none'.
534
+ *
535
+ * Recorded on every interaction (RN-8) so low screen coverage is DIAGNOSABLE rather
536
+ * than merely visible. Production shows 13.8% of Android interactions carrying a
537
+ * screen and 0.5% on iOS, and those numbers cannot today distinguish three different
538
+ * causes: the app never instrumented anything, the app instrumented manually and the
539
+ * value never reached interactions, or auto-detection ran and failed. Those have
540
+ * different owners and different fixes, and guessing between them is how a P0 stays
541
+ * open.
542
+ */
543
+ getCurrentScreenSource() {
544
+ return this._currentScreen().source;
545
+ }
546
+
547
+ /**
548
+ * LAST WRITER WINS between the manual beacon and auto-detection.
549
+ *
550
+ * A screen declared by `<ScaleBunScreen name>` stays current until something else
551
+ * declares a different one — that is what mounting a screen means. It does not
552
+ * expire, which is the correction here: the previous rule returned the manual name
553
+ * only while it was under 2000 ms old and then silently fell back to the auto value,
554
+ * which in a manually-instrumented app is `null`.
555
+ *
556
+ * Comparing timestamps rather than hard-coding a priority also handles the real
557
+ * interleaving: an app can use React Navigation for most screens AND drop a manual
558
+ * beacon on one modal, and whichever spoke last is the truth.
559
+ */
560
+ _currentScreen() {
561
+ const manual = this._manualScreen;
562
+ const auto = this._autoScreen;
563
+ if (manual && (!auto || this._manualScreenTs >= this._autoScreenTs)) {
564
+ return {
565
+ name: manual,
566
+ source: 'manual'
567
+ };
506
568
  }
507
- return this._autoScreen;
569
+ if (auto) return {
570
+ name: auto,
571
+ source: 'navigation'
572
+ };
573
+ return {
574
+ name: null,
575
+ source: 'none'
576
+ };
508
577
  }
509
578
 
510
579
  // ─── Internal ───────────────────────────────────────────────────────
@@ -24,6 +24,7 @@
24
24
  */
25
25
 
26
26
  import { installNetworkMonitoring } from './index';
27
+ import { isThirdPartyBeacon } from './thirdParty';
27
28
  import { reportNetworkRequest } from '../replay/integrations/network/networkAdapter';
28
29
  import { SessionManager } from '../session/SessionManager';
29
30
  import { getTrackedScreen } from '../../debug/screenTracking';
@@ -44,12 +45,28 @@ export class NetworkFeature {
44
45
  // self-capture loop. Apply the SAME isSdkInternalUrl filter the replay
45
46
  // branch below already uses so genuine app requests still flow through.
46
47
  const isInternal = isSdkInternalUrl(data.url, this.options.sdkBaseUrl);
47
- if (!isInternal) {
48
+ /**
49
+ * Beacons and denied URLs leave NO trace, in any lane.
50
+ *
51
+ * A network request reaches three places from here: the analytics echo
52
+ * (`$network_request`), the local replay pipeline, and the backend network endpoint
53
+ * that becomes the `network_requests` row the session panel renders. Filtering one
54
+ * and not the others is how a request half-disappears and two counts of the same
55
+ * call stop agreeing -- so this is applied to all three.
56
+ *
57
+ * Kept separate from `isInternal` rather than folded into it: the SDK's own uploads
58
+ * are already excluded from two of the three lanes and DELIBERATELY still reach the
59
+ * local replay pipeline. Widening that here would be an unrelated behaviour change
60
+ * riding along on a filter.
61
+ */
62
+ const isBeaconOrDenied = this.options.captureThirdParty !== true && isThirdPartyBeacon(data.url, this.options.thirdPartyHosts) || matchesDenyUrl(data.url, this.options.denyUrls);
63
+ const isExcluded = isInternal || isBeaconOrDenied;
64
+ if (!isExcluded) {
48
65
  this.trackFn('$network_request', data);
49
66
  }
50
67
 
51
68
  // Bridge the raw network event into the Session Replay pipeline
52
- if (data.type === 'network') {
69
+ if (data.type === 'network' && !isBeaconOrDenied) {
53
70
  reportNetworkRequest({
54
71
  method: data.method,
55
72
  url: data.url,
@@ -72,8 +89,10 @@ export class NetworkFeature {
72
89
  // (correlated by sessionId+timestamp), so we no longer emit
73
90
  // NETWORK_REQUEST_* replay events here (that produced duplicate rows
74
91
  // and count divergence between replay_events and network_requests).
75
- // Skip SDK's own upload calls to prevent recursive event spam.
76
- if (!isInternal) {
92
+ // Skip SDK's own upload calls to prevent recursive event spam, and anything the
93
+ // beacon/denyUrls filter excluded -- this is the lane that becomes the
94
+ // network_requests row a person sees on the session panel.
95
+ if (!isExcluded) {
77
96
  const sessionManager = SessionManager.getExistingInstance();
78
97
  if (sessionManager) {
79
98
  const backendTransport = sessionManager.getBackendTransport();
@@ -107,6 +126,21 @@ export class NetworkFeature {
107
126
  }
108
127
  }
109
128
 
129
+ /**
130
+ * Substring or RegExp, the same rule as the web SDK's `matchUrl`.
131
+ *
132
+ * Spelled out rather than shared because the two SDKs have no common runtime package, and the
133
+ * rule is two lines. What must NOT differ is the beacon list, which is why that one is a
134
+ * byte-identical vendored copy with a hash gate rather than a retyped constant.
135
+ */
136
+ function matchesDenyUrl(url, patterns) {
137
+ if (!patterns || !patterns.length || !url) return false;
138
+ for (const p of patterns) {
139
+ if (typeof p === 'string' ? url.includes(p) : p.test(url)) return true;
140
+ }
141
+ return false;
142
+ }
143
+
110
144
  // Returns true if the URL is a ScaleBun SDK internal endpoint that must not be
111
145
  // captured — doing so would cause recursive network event spam.
112
146
  function isSdkInternalUrl(url, sdkBaseUrl) {
@@ -0,0 +1,83 @@
1
+ /**
2
+ * THIRD-PARTY BEACON HOSTS — the one list, and the one matcher, for every layer that drops them.
3
+ *
4
+ * WHAT THIS IS FOR
5
+ * A session on an instrumented site spends most of its network tab on traffic the app did not
6
+ * meaningfully make: analytics collectors, ad pixels, IP-enrichment calls, tag managers. It is
7
+ * captured, uploaded, stored and rendered, and none of it answers a question anyone asks a session
8
+ * replay. One real sign-in screen was 27 requests, most of them beacons.
9
+ *
10
+ * It is not only clutter. Session health scores `failedRequests` and `slowRequests`, so a tracker
11
+ * killed by an ad blocker (a transport error) or one that is simply slow moves a number that is
12
+ * supposed to describe the APP. That is the part that makes this a correctness fix rather than a
13
+ * tidy-up.
14
+ *
15
+ * WHY HOSTS ONLY, NEVER PATHS
16
+ * The obvious rules from looking at a network tab are `/collect`, `/p`, `/api/v2/pixel`. Every one
17
+ * of them is a path that a customer's own API is allowed to have, and dropping a real API call is
18
+ * far worse than keeping a beacon: the noise is visible and annoying, the missing request is
19
+ * invisible and misleading. So matching is on the HOSTNAME, with a dot boundary, and a request to
20
+ * `/collect` on the app's own origin is kept — correctly.
21
+ *
22
+ * WHY IT IS A SUFFIX LIST AND NOT A REGEX
23
+ * Two reasons, both practical. It is applied inside an SDK whose eager bundle is measured in bytes,
24
+ * and a comma-joined string of suffixes compresses to almost nothing next to an equivalent pattern
25
+ * source. And it has to be COPIED, byte for byte, into the React Native SDK and into the backend's
26
+ * ingest path — a list with no syntax is a list that cannot drift in meaning between three
27
+ * languages' worth of tooling. `third-party:parity` hashes all three copies.
28
+ *
29
+ * ZERO IMPORTS, DELIBERATELY. That is what makes the byte-identical copy possible. If an edit here
30
+ * ever needs an import, the parity gate is the thing that will say so — keep the module pure rather
31
+ * than weakening the gate.
32
+ *
33
+ * WHAT IS DELIBERATELY NOT ON THE LIST
34
+ * The rule is: a host whose ONLY purpose is to receive telemetry. Everything else stays, because a
35
+ * dropped request is invisible and a kept one is merely noisy.
36
+ *
37
+ * - Feature-flag and experiment services (LaunchDarkly, Optimizely, Split). Their RESPONSES
38
+ * change what the app does, so a failed flag fetch is an application incident and the single
39
+ * most useful request in the session.
40
+ * - Chat and support widgets (Intercom, Zendesk, Drift). A broken widget is a broken feature the
41
+ * user can see.
42
+ * - `facebook.com` as a whole. `graph.facebook.com` is an API real apps call; only the pixel
43
+ * delivery hosts are listed.
44
+ * - CDNs of any kind, even ones that mostly serve tags: they also serve application assets, and
45
+ * dropping them would hide a real asset failure.
46
+ *
47
+ * When in doubt, leave it out. `denyUrls` stays the per-app escape hatch in both SDKs, and it can
48
+ * do what this list will not.
49
+ */
50
+
51
+ /**
52
+ * Known telemetry, advertising and enrichment endpoints, as hostname suffixes.
53
+ *
54
+ * One comma-joined string rather than an array: identical meaning, materially smaller once gzipped,
55
+ * and it keeps the copy in three repos a single line to compare by eye.
56
+ */
57
+ export const THIRD_PARTY_BEACON_HOSTS = 'google-analytics.com,googletagmanager.com,analytics.google.com,doubleclick.net,googlesyndication.com,' + 'googleadservices.com,app-measurement.com,facebook.net,analytics.tiktok.com,' + 'ads-twitter.com,analytics.twitter.com,ct.pinterest.com,bat.bing.com,px.ads.linkedin.com,tr.snapchat.com,' + 'criteo.com,criteo.net,taboola.com,outbrain.com,adroll.com,adsrvr.org,quantserve.com,scorecardresearch.com,' + 'hotjar.com,hotjar.io,clarity.ms,fullstory.com,mouseflow.com,crazyegg.com,luckyorange.com,inspectlet.com,' + 'segment.io,mixpanel.com,amplitude.com,heap.io,heapanalytics.com,rudderstack.com,posthog.com,' + 'matomo.cloud,plausible.io,simpleanalytics.com,statcounter.com,' + 'appsflyer.com,adjust.com,branch.io,' + 'ipify.org,ipinfo.io,ipapi.co,ip-api.com,geolocation-db.com,cloudflareinsights.com,' + 'nr-data.net,browser-intake-datadoghq.com,sentry.io,bugsnag.com,rollbar.com';
58
+
59
+ /**
60
+ * True when `url` is addressed to a known beacon host.
61
+ *
62
+ * Deliberately total: an unparseable or relative URL is NOT third party. A relative URL is
63
+ * same-origin by definition — the app's own call — and guessing on garbage input would drop real
64
+ * requests for no benefit.
65
+ */
66
+ export function isThirdPartyBeacon(url, hosts) {
67
+ if (!url) return false;
68
+ let host = '';
69
+ try {
70
+ // A relative URL throws here, which is the answer we want: it is first-party.
71
+ host = new URL(url).hostname.toLowerCase();
72
+ } catch {
73
+ return false;
74
+ }
75
+ if (!host) return false;
76
+ for (const suffix of (hosts ?? THIRD_PARTY_BEACON_HOSTS).split(',')) {
77
+ // Dot boundary, so `notevil-facebook.com` does not match `facebook.com` while
78
+ // `connect.facebook.com` does. The bare-equality arm covers the apex domain itself.
79
+ if (host === suffix || host.endsWith('.' + suffix)) return true;
80
+ }
81
+ return false;
82
+ }
83
+ //# sourceMappingURL=thirdParty.js.map