@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.
- package/android/build.gradle +8 -0
- package/android/src/androidTest/java/com/scalebun/rn/ota/ScaleBunOtaVerifierInstrumentedTest.kt +112 -0
- package/android/src/main/java/com/scalebun/rn/ota/BundleDownloader.kt +10 -0
- package/android/src/main/java/com/scalebun/rn/ota/DeviceIntegrity.kt +75 -0
- package/android/src/main/java/com/scalebun/rn/ota/OtaProtocol.kt +244 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaKeyRegistry.kt +100 -0
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaModule.kt +132 -62
- package/android/src/main/java/com/scalebun/rn/ota/ScaleBunOtaReleaseVerifier.kt +129 -0
- package/android/src/main/java/com/scalebun/rn/ota/SlotManager.kt +80 -1
- package/android/src/main/java/com/scalebun/rn/ota/TlsPinning.kt +118 -0
- package/android/src/oldarch/java/com/scalebun/rn/ota/ScaleBunOtaSpec.kt +6 -8
- package/dist/scalebun.full.js +964 -533
- package/dist/scalebun.slim.js +962 -531
- package/ios/Ota/OtaProtocol.swift +242 -0
- package/ios/Ota/OtaSlotManager.swift +26 -0
- package/ios/Ota/ScaleBunOtaBridge.mm +6 -3
- package/ios/Ota/ScaleBunOtaKeyRegistry.swift +147 -0
- package/ios/Ota/ScaleBunOtaModule.swift +125 -53
- package/ios/Ota/ScaleBunOtaReleaseVerifier.swift +115 -0
- package/ios/Ota/ScaleBunOtaVerifierTests.swift +92 -0
- package/ios/ReplaySdk.swift +65 -2
- package/lib/commonjs/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/commonjs/core/config/schema.js +19 -12
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/features/journey/ScaleBunDebugRoot.js +293 -5
- package/lib/commonjs/features/journey/ScaleBunScrollView.js +28 -0
- package/lib/commonjs/features/journey/autoInstrumentScroll.js +161 -0
- package/lib/commonjs/features/journey/interactionProtocol.js +131 -10
- package/lib/commonjs/features/journey/scrollContext.js +146 -0
- package/lib/commonjs/features/journey/targetGeometry.js +164 -0
- package/lib/commonjs/features/journey/touchTarget.js +152 -24
- package/lib/commonjs/features/journey/uiState.js +81 -1
- package/lib/commonjs/features/navigation/AutoScreenDetector.js +74 -5
- package/lib/commonjs/features/network/NetworkFeature.js +39 -5
- package/lib/commonjs/features/network/thirdParty.js +90 -0
- package/lib/commonjs/features/ota/OtaOrchestrator.js +175 -80
- package/lib/commonjs/features/session/SessionManager.js +63 -0
- package/lib/commonjs/public/ScaleBunFacade.js +23 -57
- package/lib/module/bootstrap/SDKBootstrapper.js +30 -0
- package/lib/module/core/config/schema.js +19 -12
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/features/journey/ScaleBunDebugRoot.js +295 -7
- package/lib/module/features/journey/ScaleBunScrollView.js +28 -0
- package/lib/module/features/journey/autoInstrumentScroll.js +155 -0
- package/lib/module/features/journey/interactionProtocol.js +128 -8
- package/lib/module/features/journey/scrollContext.js +135 -0
- package/lib/module/features/journey/targetGeometry.js +155 -0
- package/lib/module/features/journey/touchTarget.js +151 -24
- package/lib/module/features/journey/uiState.js +78 -1
- package/lib/module/features/navigation/AutoScreenDetector.js +74 -5
- package/lib/module/features/network/NetworkFeature.js +38 -4
- package/lib/module/features/network/thirdParty.js +83 -0
- package/lib/module/features/ota/OtaOrchestrator.js +175 -80
- package/lib/module/features/session/SessionManager.js +63 -0
- package/lib/module/public/ScaleBunFacade.js +23 -57
- package/lib/typescript/core/config/schema.d.ts +2 -3
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/lib/typescript/features/journey/autoInstrumentScroll.d.ts +46 -0
- package/lib/typescript/features/journey/interactionProtocol.d.ts +139 -1
- package/lib/typescript/features/journey/scrollContext.d.ts +85 -0
- package/lib/typescript/features/journey/targetGeometry.d.ts +114 -0
- package/lib/typescript/features/journey/touchTarget.d.ts +148 -15
- package/lib/typescript/features/journey/uiState.d.ts +39 -0
- package/lib/typescript/features/navigation/AutoScreenDetector.d.ts +48 -2
- package/lib/typescript/features/network/index.d.ts +18 -0
- package/lib/typescript/features/network/thirdParty.d.ts +65 -0
- package/lib/typescript/features/ota/OtaOrchestrator.d.ts +12 -8
- package/lib/typescript/features/ota/OtaTypes.d.ts +22 -0
- package/lib/typescript/features/session/SessionManager.d.ts +55 -0
- package/lib/typescript/public/ScaleBunFacade.d.ts +5 -11
- package/lib/typescript/public/types.d.ts +10 -0
- package/lib/typescript/specs/NativeScaleBunOta.d.ts +32 -20
- package/package.json +3 -13
- package/scalebun-react-native.podspec +4 -0
- package/src/bootstrap/SDKBootstrapper.ts +40 -0
- package/src/core/config/schema.ts +21 -9
- package/src/core/constants/version.ts +1 -1
- package/src/features/journey/ScaleBunDebugRoot.tsx +288 -7
- package/src/features/journey/ScaleBunScrollView.tsx +29 -0
- package/src/features/journey/autoInstrumentScroll.ts +155 -0
- package/src/features/journey/interactionProtocol.ts +188 -10
- package/src/features/journey/scrollContext.ts +137 -0
- package/src/features/journey/targetGeometry.ts +176 -0
- package/src/features/journey/touchTarget.ts +237 -26
- package/src/features/journey/uiState.ts +80 -1
- package/src/features/navigation/AutoScreenDetector.ts +65 -5
- package/src/features/network/NetworkFeature.ts +41 -4
- package/src/features/network/index.ts +18 -0
- package/src/features/network/thirdParty.ts +92 -0
- package/src/features/ota/OtaOrchestrator.ts +190 -90
- package/src/features/ota/OtaTypes.ts +23 -1
- package/src/features/session/SessionManager.ts +113 -0
- package/src/public/ScaleBunFacade.ts +23 -69
- package/src/public/types.ts +10 -0
- package/src/specs/NativeScaleBunOta.ts +35 -20
- package/lib/commonjs/features/ota/crypto/builtinVerifier.js +0 -248
- package/lib/commonjs/features/ota/crypto/loadEd25519.js +0 -40
- package/lib/commonjs/features/ota/crypto/loadSha512.js +0 -40
- package/lib/commonjs/features/ota/crypto/nativeVerifier.js +0 -121
- package/lib/commonjs/features/ota/signature.js +0 -175
- package/lib/module/features/ota/crypto/builtinVerifier.js +0 -240
- package/lib/module/features/ota/crypto/loadEd25519.js +0 -34
- package/lib/module/features/ota/crypto/loadSha512.js +0 -34
- package/lib/module/features/ota/crypto/nativeVerifier.js +0 -113
- package/lib/module/features/ota/signature.js +0 -169
- package/lib/typescript/features/ota/crypto/builtinVerifier.d.ts +0 -53
- package/lib/typescript/features/ota/crypto/loadEd25519.d.ts +0 -30
- package/lib/typescript/features/ota/crypto/loadSha512.d.ts +0 -15
- package/lib/typescript/features/ota/crypto/nativeVerifier.d.ts +0 -35
- package/lib/typescript/features/ota/signature.d.ts +0 -81
- package/src/features/ota/crypto/builtinVerifier.ts +0 -257
- package/src/features/ota/crypto/loadEd25519.ts +0 -41
- package/src/features/ota/crypto/loadSha512.ts +0 -35
- package/src/features/ota/crypto/nativeVerifier.ts +0 -117
- package/src/features/ota/signature.ts +0 -206
|
@@ -49,6 +49,15 @@ export class AutoScreenDetector {
|
|
|
49
49
|
private _manualScreen: string | null = null;
|
|
50
50
|
private _manualScreenTs = 0;
|
|
51
51
|
private _autoScreen: string | null = null;
|
|
52
|
+
/**
|
|
53
|
+
* When `_autoScreen` was last written.
|
|
54
|
+
*
|
|
55
|
+
* Needed so `getCurrentScreen()` can prefer whichever source spoke MOST RECENTLY.
|
|
56
|
+
* Without a timestamp the only comparison available was "is the manual value still
|
|
57
|
+
* fresh", which is a different question and gave the wrong answer — see the note on
|
|
58
|
+
* `_currentScreen()`.
|
|
59
|
+
*/
|
|
60
|
+
private _autoScreenTs = 0;
|
|
52
61
|
/** `name#key` of the last auto-detected screen VISIT — see _extractActiveVisit. */
|
|
53
62
|
private _autoVisitSig: string | null = null;
|
|
54
63
|
private _navigationRef: any = null;
|
|
@@ -124,6 +133,7 @@ export class AutoScreenDetector {
|
|
|
124
133
|
if (this.isManualScreenActive()) return;
|
|
125
134
|
if (name !== this._autoScreen) {
|
|
126
135
|
this._autoScreen = name;
|
|
136
|
+
this._autoScreenTs = Date.now();
|
|
127
137
|
this._autoVisitSig = name; // native lifecycle carries no route key — name-only visit
|
|
128
138
|
this._emitScreenChange(name, 'heuristic');
|
|
129
139
|
}
|
|
@@ -131,8 +141,19 @@ export class AutoScreenDetector {
|
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
144
|
+
* Should a WRITER defer to the manual screen right now?
|
|
145
|
+
*
|
|
146
|
+
* THIS IS A PRECEDENCE GATE, NOT A VALIDITY TEST, and conflating the two was a real
|
|
147
|
+
* bug. Its job is what the cooldown constant says: for a short window after a manual
|
|
148
|
+
* beacon fires, an auto source must not overwrite it — the poller runs every 1500 ms
|
|
149
|
+
* and would otherwise clobber a fresh declaration with whatever it inferred.
|
|
150
|
+
*
|
|
151
|
+
* `getCurrentScreen()` used to reuse this as "is the manual screen still the current
|
|
152
|
+
* screen", which made a declared screen READABLE for only 2000 ms. Nothing called
|
|
153
|
+
* `setManualScreen` in production, so the effect was latent — but the moment the
|
|
154
|
+
* `<ScaleBunScreen>` beacon was wired to it (RN-8), every tap more than two seconds
|
|
155
|
+
* after a screen mounted would have lost its name, which is very nearly every tap. A
|
|
156
|
+
* fix that introduced that would have been worse than the gap it closed.
|
|
136
157
|
*/
|
|
137
158
|
isManualScreenActive(): boolean {
|
|
138
159
|
if (!this._manualScreen) return false;
|
|
@@ -196,6 +217,7 @@ export class AutoScreenDetector {
|
|
|
196
217
|
const visit = this._extractActiveVisit(state);
|
|
197
218
|
if (visit && this._visitSig(visit) !== this._autoVisitSig) {
|
|
198
219
|
this._autoScreen = visit.name;
|
|
220
|
+
this._autoScreenTs = Date.now();
|
|
199
221
|
this._autoVisitSig = this._visitSig(visit);
|
|
200
222
|
this._emitScreenChange(visit.name, 'navRef');
|
|
201
223
|
}
|
|
@@ -216,6 +238,7 @@ export class AutoScreenDetector {
|
|
|
216
238
|
const visit = this._extractActiveVisit(state);
|
|
217
239
|
if (visit) {
|
|
218
240
|
this._autoScreen = visit.name;
|
|
241
|
+
this._autoScreenTs = Date.now();
|
|
219
242
|
this._autoVisitSig = this._visitSig(visit);
|
|
220
243
|
this._emitScreenChange(visit.name, 'navRef');
|
|
221
244
|
}
|
|
@@ -261,6 +284,7 @@ export class AutoScreenDetector {
|
|
|
261
284
|
const visit = this._extractActiveVisit(state);
|
|
262
285
|
if (visit && this._visitSig(visit) !== this._autoVisitSig) {
|
|
263
286
|
this._autoScreen = visit.name;
|
|
287
|
+
this._autoScreenTs = Date.now();
|
|
264
288
|
this._autoVisitSig = this._visitSig(visit);
|
|
265
289
|
this._emitScreenChange(visit.name, 'heuristic');
|
|
266
290
|
}
|
|
@@ -517,16 +541,52 @@ export class AutoScreenDetector {
|
|
|
517
541
|
this._listeners = [];
|
|
518
542
|
this._manualScreen = null;
|
|
519
543
|
this._autoScreen = null;
|
|
544
|
+
this._autoScreenTs = 0;
|
|
520
545
|
this._navigationRef = null;
|
|
521
546
|
_instance = null;
|
|
522
547
|
}
|
|
523
548
|
|
|
524
549
|
/** Get the current best-known screen name */
|
|
525
550
|
getCurrentScreen(): string | null {
|
|
526
|
-
|
|
527
|
-
|
|
551
|
+
return this._currentScreen().name;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* WHERE the current screen name came from, or 'none'.
|
|
556
|
+
*
|
|
557
|
+
* Recorded on every interaction (RN-8) so low screen coverage is DIAGNOSABLE rather
|
|
558
|
+
* than merely visible. Production shows 13.8% of Android interactions carrying a
|
|
559
|
+
* screen and 0.5% on iOS, and those numbers cannot today distinguish three different
|
|
560
|
+
* causes: the app never instrumented anything, the app instrumented manually and the
|
|
561
|
+
* value never reached interactions, or auto-detection ran and failed. Those have
|
|
562
|
+
* different owners and different fixes, and guessing between them is how a P0 stays
|
|
563
|
+
* open.
|
|
564
|
+
*/
|
|
565
|
+
getCurrentScreenSource(): 'manual' | 'navigation' | 'none' {
|
|
566
|
+
return this._currentScreen().source;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* LAST WRITER WINS between the manual beacon and auto-detection.
|
|
571
|
+
*
|
|
572
|
+
* A screen declared by `<ScaleBunScreen name>` stays current until something else
|
|
573
|
+
* declares a different one — that is what mounting a screen means. It does not
|
|
574
|
+
* expire, which is the correction here: the previous rule returned the manual name
|
|
575
|
+
* only while it was under 2000 ms old and then silently fell back to the auto value,
|
|
576
|
+
* which in a manually-instrumented app is `null`.
|
|
577
|
+
*
|
|
578
|
+
* Comparing timestamps rather than hard-coding a priority also handles the real
|
|
579
|
+
* interleaving: an app can use React Navigation for most screens AND drop a manual
|
|
580
|
+
* beacon on one modal, and whichever spoke last is the truth.
|
|
581
|
+
*/
|
|
582
|
+
private _currentScreen(): { name: string | null; source: 'manual' | 'navigation' | 'none' } {
|
|
583
|
+
const manual = this._manualScreen;
|
|
584
|
+
const auto = this._autoScreen;
|
|
585
|
+
if (manual && (!auto || this._manualScreenTs >= this._autoScreenTs)) {
|
|
586
|
+
return { name: manual, source: 'manual' };
|
|
528
587
|
}
|
|
529
|
-
return
|
|
588
|
+
if (auto) return { name: auto, source: 'navigation' };
|
|
589
|
+
return { name: null, source: 'none' };
|
|
530
590
|
}
|
|
531
591
|
|
|
532
592
|
// ─── Internal ───────────────────────────────────────────────────────
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import type { IFeature, FeatureContext } from '../../core/contracts/IFeature';
|
|
27
27
|
import { installNetworkMonitoring, type NetworkMonitoringOptions } from './index';
|
|
28
|
+
import { isThirdPartyBeacon } from './thirdParty';
|
|
28
29
|
import { reportNetworkRequest } from '../replay/integrations/network/networkAdapter';
|
|
29
30
|
import { SessionManager } from '../session/SessionManager';
|
|
30
31
|
import { getTrackedScreen } from '../../debug/screenTracking';
|
|
@@ -53,13 +54,32 @@ export class NetworkFeature implements IFeature {
|
|
|
53
54
|
// self-capture loop. Apply the SAME isSdkInternalUrl filter the replay
|
|
54
55
|
// branch below already uses so genuine app requests still flow through.
|
|
55
56
|
const isInternal = isSdkInternalUrl(data.url, this.options.sdkBaseUrl);
|
|
57
|
+
/**
|
|
58
|
+
* Beacons and denied URLs leave NO trace, in any lane.
|
|
59
|
+
*
|
|
60
|
+
* A network request reaches three places from here: the analytics echo
|
|
61
|
+
* (`$network_request`), the local replay pipeline, and the backend network endpoint
|
|
62
|
+
* that becomes the `network_requests` row the session panel renders. Filtering one
|
|
63
|
+
* and not the others is how a request half-disappears and two counts of the same
|
|
64
|
+
* call stop agreeing -- so this is applied to all three.
|
|
65
|
+
*
|
|
66
|
+
* Kept separate from `isInternal` rather than folded into it: the SDK's own uploads
|
|
67
|
+
* are already excluded from two of the three lanes and DELIBERATELY still reach the
|
|
68
|
+
* local replay pipeline. Widening that here would be an unrelated behaviour change
|
|
69
|
+
* riding along on a filter.
|
|
70
|
+
*/
|
|
71
|
+
const isBeaconOrDenied =
|
|
72
|
+
(this.options.captureThirdParty !== true &&
|
|
73
|
+
isThirdPartyBeacon(data.url, this.options.thirdPartyHosts)) ||
|
|
74
|
+
matchesDenyUrl(data.url, this.options.denyUrls);
|
|
75
|
+
const isExcluded = isInternal || isBeaconOrDenied;
|
|
56
76
|
|
|
57
|
-
if (!
|
|
77
|
+
if (!isExcluded) {
|
|
58
78
|
this.trackFn('$network_request', data);
|
|
59
79
|
}
|
|
60
80
|
|
|
61
81
|
// Bridge the raw network event into the Session Replay pipeline
|
|
62
|
-
if (data.type === 'network') {
|
|
82
|
+
if (data.type === 'network' && !isBeaconOrDenied) {
|
|
63
83
|
reportNetworkRequest({
|
|
64
84
|
method: data.method,
|
|
65
85
|
url: data.url,
|
|
@@ -82,8 +102,10 @@ export class NetworkFeature implements IFeature {
|
|
|
82
102
|
// (correlated by sessionId+timestamp), so we no longer emit
|
|
83
103
|
// NETWORK_REQUEST_* replay events here (that produced duplicate rows
|
|
84
104
|
// and count divergence between replay_events and network_requests).
|
|
85
|
-
// Skip SDK's own upload calls to prevent recursive event spam
|
|
86
|
-
|
|
105
|
+
// Skip SDK's own upload calls to prevent recursive event spam, and anything the
|
|
106
|
+
// beacon/denyUrls filter excluded -- this is the lane that becomes the
|
|
107
|
+
// network_requests row a person sees on the session panel.
|
|
108
|
+
if (!isExcluded) {
|
|
87
109
|
const sessionManager = SessionManager.getExistingInstance();
|
|
88
110
|
if (sessionManager) {
|
|
89
111
|
const backendTransport = sessionManager.getBackendTransport();
|
|
@@ -119,6 +141,21 @@ export class NetworkFeature implements IFeature {
|
|
|
119
141
|
}
|
|
120
142
|
}
|
|
121
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Substring or RegExp, the same rule as the web SDK's `matchUrl`.
|
|
146
|
+
*
|
|
147
|
+
* Spelled out rather than shared because the two SDKs have no common runtime package, and the
|
|
148
|
+
* rule is two lines. What must NOT differ is the beacon list, which is why that one is a
|
|
149
|
+
* byte-identical vendored copy with a hash gate rather than a retyped constant.
|
|
150
|
+
*/
|
|
151
|
+
function matchesDenyUrl(url: string, patterns?: (string | RegExp)[]): boolean {
|
|
152
|
+
if (!patterns || !patterns.length || !url) return false;
|
|
153
|
+
for (const p of patterns) {
|
|
154
|
+
if (typeof p === 'string' ? url.includes(p) : p.test(url)) return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
122
159
|
// Returns true if the URL is a ScaleBun SDK internal endpoint that must not be
|
|
123
160
|
// captured — doing so would cause recursive network event spam.
|
|
124
161
|
function isSdkInternalUrl(url: string, sdkBaseUrl?: string): boolean {
|
|
@@ -18,6 +18,24 @@ export interface NetworkMonitoringOptions {
|
|
|
18
18
|
redactNetwork?: boolean;
|
|
19
19
|
/** SDK backend base URL — used to filter out SDK's own upload calls from capture */
|
|
20
20
|
sdkBaseUrl?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Never capture requests whose URL matches (substring or RegExp).
|
|
23
|
+
*
|
|
24
|
+
* Parity with the web SDK, which has had this since F-NET. Mobile had no way to exclude a
|
|
25
|
+
* URL at all, so an app that wanted one endpoint out of its replays had no answer.
|
|
26
|
+
*/
|
|
27
|
+
denyUrls?: (string | RegExp)[];
|
|
28
|
+
/**
|
|
29
|
+
* Keep known telemetry beacons: analytics collectors, ad pixels, IP-enrichment calls.
|
|
30
|
+
*
|
|
31
|
+
* Default false — they are dropped before capture. They are not the app, they cost the
|
|
32
|
+
* device real battery and cellular data to upload, and a beacon that fails counts against
|
|
33
|
+
* the SESSION HEALTH score. Matching is by hostname only, never by path, so an app route
|
|
34
|
+
* called /collect is never touched. See features/network/thirdParty.ts.
|
|
35
|
+
*/
|
|
36
|
+
captureThirdParty?: boolean;
|
|
37
|
+
/** Replace the built-in beacon host list wholesale. Comma-joined hostname suffixes. */
|
|
38
|
+
thirdPartyHosts?: string;
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
export function installNetworkMonitoring(
|
|
@@ -0,0 +1,92 @@
|
|
|
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 =
|
|
58
|
+
'google-analytics.com,googletagmanager.com,analytics.google.com,doubleclick.net,googlesyndication.com,' +
|
|
59
|
+
'googleadservices.com,app-measurement.com,facebook.net,analytics.tiktok.com,' +
|
|
60
|
+
'ads-twitter.com,analytics.twitter.com,ct.pinterest.com,bat.bing.com,px.ads.linkedin.com,tr.snapchat.com,' +
|
|
61
|
+
'criteo.com,criteo.net,taboola.com,outbrain.com,adroll.com,adsrvr.org,quantserve.com,scorecardresearch.com,' +
|
|
62
|
+
'hotjar.com,hotjar.io,clarity.ms,fullstory.com,mouseflow.com,crazyegg.com,luckyorange.com,inspectlet.com,' +
|
|
63
|
+
'segment.io,mixpanel.com,amplitude.com,heap.io,heapanalytics.com,rudderstack.com,posthog.com,' +
|
|
64
|
+
'matomo.cloud,plausible.io,simpleanalytics.com,statcounter.com,' +
|
|
65
|
+
'appsflyer.com,adjust.com,branch.io,' +
|
|
66
|
+
'ipify.org,ipinfo.io,ipapi.co,ip-api.com,geolocation-db.com,cloudflareinsights.com,' +
|
|
67
|
+
'nr-data.net,browser-intake-datadoghq.com,sentry.io,bugsnag.com,rollbar.com';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* True when `url` is addressed to a known beacon host.
|
|
71
|
+
*
|
|
72
|
+
* Deliberately total: an unparseable or relative URL is NOT third party. A relative URL is
|
|
73
|
+
* same-origin by definition — the app's own call — and guessing on garbage input would drop real
|
|
74
|
+
* requests for no benefit.
|
|
75
|
+
*/
|
|
76
|
+
export function isThirdPartyBeacon(url: string, hosts?: string): boolean {
|
|
77
|
+
if (!url) return false;
|
|
78
|
+
let host = '';
|
|
79
|
+
try {
|
|
80
|
+
// A relative URL throws here, which is the answer we want: it is first-party.
|
|
81
|
+
host = new URL(url).hostname.toLowerCase();
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (!host) return false;
|
|
86
|
+
for (const suffix of (hosts ?? THIRD_PARTY_BEACON_HOSTS).split(',')) {
|
|
87
|
+
// Dot boundary, so `notevil-facebook.com` does not match `facebook.com` while
|
|
88
|
+
// `connect.facebook.com` does. The bare-equality arm covers the apex domain itself.
|
|
89
|
+
if (host === suffix || host.endsWith('.' + suffix)) return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|