@scalebun/react-native 1.10.5 → 1.10.6
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/dist/scalebun.full.js +54 -1
- package/dist/scalebun.slim.js +54 -1
- package/ios/Core/DeviceIdentity.swift +50 -0
- package/ios/Profiler/ScaleBunProfilerModule.swift +2 -8
- package/ios/ReplaySdk.swift +3 -1
- package/lib/commonjs/analytics/EventTracker.js +22 -0
- package/lib/commonjs/core/constants/version.js +1 -1
- package/lib/commonjs/core/context/device.js +5 -0
- package/lib/commonjs/public/ScaleBunFacade.js +34 -0
- package/lib/module/analytics/EventTracker.js +22 -0
- package/lib/module/core/constants/version.js +1 -1
- package/lib/module/core/context/device.js +5 -0
- package/lib/module/public/ScaleBunFacade.js +35 -1
- package/lib/typescript/analytics/EventTracker.d.ts +15 -0
- package/lib/typescript/core/constants/version.d.ts +1 -1
- package/lib/typescript/core/context/device.d.ts +1 -0
- package/lib/typescript/public/ScaleBunFacade.d.ts +8 -0
- package/package.json +1 -1
- package/src/analytics/EventTracker.ts +19 -0
- package/src/core/constants/version.ts +1 -1
- package/src/core/context/device.ts +6 -0
- package/src/public/ScaleBunFacade.ts +41 -1
package/dist/scalebun.full.js
CHANGED
|
@@ -605,7 +605,7 @@ var SDK_VERSION;
|
|
|
605
605
|
var init_version = __esm({
|
|
606
606
|
"lib/module/core/constants/version.js"() {
|
|
607
607
|
"use strict";
|
|
608
|
-
SDK_VERSION = "1.10.
|
|
608
|
+
SDK_VERSION = "1.10.6";
|
|
609
609
|
}
|
|
610
610
|
});
|
|
611
611
|
|
|
@@ -647,6 +647,15 @@ function staticDeviceContext() {
|
|
|
647
647
|
if (c.Model) out.device_model = String(c.Model);
|
|
648
648
|
if (c.Manufacturer) out.manufacturer = String(c.Manufacturer);
|
|
649
649
|
else if (c.Brand) out.manufacturer = String(c.Brand);
|
|
650
|
+
else if (out.platform_os === "ios") out.manufacturer = "Apple";
|
|
651
|
+
return out;
|
|
652
|
+
}
|
|
653
|
+
function nativeDeviceContext(info) {
|
|
654
|
+
const out = {};
|
|
655
|
+
if (!info) return out;
|
|
656
|
+
if (info.osVersion) out.os_version = info.osVersion;
|
|
657
|
+
if (info.deviceModel) out.device_model = info.deviceModel;
|
|
658
|
+
if (info.manufacturer) out.manufacturer = info.manufacturer;
|
|
650
659
|
return out;
|
|
651
660
|
}
|
|
652
661
|
function mergeDeviceContext(configured, detected = staticDeviceContext()) {
|
|
@@ -11232,6 +11241,27 @@ var init_EventTracker = __esm({
|
|
|
11232
11241
|
}
|
|
11233
11242
|
this.persistQueue();
|
|
11234
11243
|
}
|
|
11244
|
+
/**
|
|
11245
|
+
* Fold in device facts that were not knowable at init.
|
|
11246
|
+
*
|
|
11247
|
+
* `Platform.constants` answers Android synchronously, so its model and manufacturer ride the
|
|
11248
|
+
* very first event. iOS exposes neither there — only the native bridge knows, and the bridge is
|
|
11249
|
+
* async and may not be installed at all. Without this the iOS half of every install reported no
|
|
11250
|
+
* model whatsoever.
|
|
11251
|
+
*
|
|
11252
|
+
* `buildEnvelope` reads `cfg.context` per event, so applying it here reaches every event from
|
|
11253
|
+
* the next one onward. Detected keys WIN over whatever the integrator configured, for the same
|
|
11254
|
+
* reason `mergeDeviceContext` inverts the usual precedence: a measurement must not be
|
|
11255
|
+
* overridable by a guess. Empty patches are ignored so a bridge that answered with nothing
|
|
11256
|
+
* cannot blank a value the static path already found.
|
|
11257
|
+
*/
|
|
11258
|
+
applyDetectedContext(patch) {
|
|
11259
|
+
if (!patch || Object.keys(patch).length === 0) return;
|
|
11260
|
+
this.cfg.context = {
|
|
11261
|
+
...this.cfg.context ?? {},
|
|
11262
|
+
...patch
|
|
11263
|
+
};
|
|
11264
|
+
}
|
|
11235
11265
|
// ─── internals ─────────────────────────────────────────────────────────────
|
|
11236
11266
|
buildEnvelope(eventName, properties) {
|
|
11237
11267
|
const ctx = this.cfg.context ?? {};
|
|
@@ -17408,6 +17438,7 @@ var SDKBootstrapper = class {
|
|
|
17408
17438
|
|
|
17409
17439
|
// lib/module/public/ScaleBunFacade.js
|
|
17410
17440
|
init_EventTracker();
|
|
17441
|
+
init_bridgeAdapter();
|
|
17411
17442
|
init_automaticEvents();
|
|
17412
17443
|
|
|
17413
17444
|
// lib/module/analytics/eventLane.js
|
|
@@ -19925,6 +19956,8 @@ var ScaleBunFacade = class {
|
|
|
19925
19956
|
* the whole time. The SDK knows the device; the integrator is guessing at it.
|
|
19926
19957
|
* Everything outside the detected keys is still theirs.
|
|
19927
19958
|
*/
|
|
19959
|
+
// Everything Platform.constants knows. On iOS that is neither the model nor the
|
|
19960
|
+
// manufacturer, so the bridge completes it below once it answers.
|
|
19928
19961
|
context: mergeDeviceContext(rawConfig?.context),
|
|
19929
19962
|
// Use the same foreground id as replay/journey capture. The
|
|
19930
19963
|
// callback is resolved per event because the session subsystem
|
|
@@ -19938,6 +19971,7 @@ var ScaleBunFacade = class {
|
|
|
19938
19971
|
});
|
|
19939
19972
|
this._eventTracker.start();
|
|
19940
19973
|
__DEV__ && logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
|
|
19974
|
+
this._applyNativeDeviceContext(this._eventTracker);
|
|
19941
19975
|
this._captureInstallReferrer(this._eventTracker);
|
|
19942
19976
|
const tracker = this._eventTracker;
|
|
19943
19977
|
this._configManager = new ConfigManager({
|
|
@@ -19956,6 +19990,25 @@ var ScaleBunFacade = class {
|
|
|
19956
19990
|
logger.warn("[ScaleBun] Failed to start envelope event tracking:", err?.message);
|
|
19957
19991
|
}
|
|
19958
19992
|
}
|
|
19993
|
+
/**
|
|
19994
|
+
* Fold the native bridge's device facts into the analytics context once it answers.
|
|
19995
|
+
*
|
|
19996
|
+
* Detected values never overwrite a populated one with an empty one — `nativeDeviceContext`
|
|
19997
|
+
* drops absent keys and `applyDetectedContext` ignores an empty patch — so a bridge that
|
|
19998
|
+
* cannot answer leaves whatever `Platform.constants` already found.
|
|
19999
|
+
*/
|
|
20000
|
+
_applyNativeDeviceContext(tracker) {
|
|
20001
|
+
Promise.resolve().then(() => bridgeAdapter.getDeviceInfo()).then((info) => {
|
|
20002
|
+
if (!info) return;
|
|
20003
|
+
tracker.applyDetectedContext(nativeDeviceContext({
|
|
20004
|
+
osVersion: info.osVersion,
|
|
20005
|
+
deviceModel: info.deviceModel
|
|
20006
|
+
// iOS ships no manufacturer over the bridge; it is a constant there and the
|
|
20007
|
+
// static path already set it. Android's arrives via Platform.constants.
|
|
20008
|
+
}));
|
|
20009
|
+
}).catch(() => {
|
|
20010
|
+
});
|
|
20011
|
+
}
|
|
19959
20012
|
/**
|
|
19960
20013
|
* Android-only: read the Play Install Referrer once per install and route it into
|
|
19961
20014
|
* attribution. `scalebun_click_id` → the deterministic click path; the full
|
package/dist/scalebun.slim.js
CHANGED
|
@@ -600,7 +600,7 @@ var SDK_VERSION;
|
|
|
600
600
|
var init_version = __esm({
|
|
601
601
|
"lib/module/core/constants/version.js"() {
|
|
602
602
|
"use strict";
|
|
603
|
-
SDK_VERSION = "1.10.
|
|
603
|
+
SDK_VERSION = "1.10.6";
|
|
604
604
|
}
|
|
605
605
|
});
|
|
606
606
|
|
|
@@ -642,6 +642,15 @@ function staticDeviceContext() {
|
|
|
642
642
|
if (c.Model) out.device_model = String(c.Model);
|
|
643
643
|
if (c.Manufacturer) out.manufacturer = String(c.Manufacturer);
|
|
644
644
|
else if (c.Brand) out.manufacturer = String(c.Brand);
|
|
645
|
+
else if (out.platform_os === "ios") out.manufacturer = "Apple";
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
function nativeDeviceContext(info) {
|
|
649
|
+
const out = {};
|
|
650
|
+
if (!info) return out;
|
|
651
|
+
if (info.osVersion) out.os_version = info.osVersion;
|
|
652
|
+
if (info.deviceModel) out.device_model = info.deviceModel;
|
|
653
|
+
if (info.manufacturer) out.manufacturer = info.manufacturer;
|
|
645
654
|
return out;
|
|
646
655
|
}
|
|
647
656
|
function mergeDeviceContext(configured, detected = staticDeviceContext()) {
|
|
@@ -6528,6 +6537,27 @@ var init_EventTracker = __esm({
|
|
|
6528
6537
|
}
|
|
6529
6538
|
this.persistQueue();
|
|
6530
6539
|
}
|
|
6540
|
+
/**
|
|
6541
|
+
* Fold in device facts that were not knowable at init.
|
|
6542
|
+
*
|
|
6543
|
+
* `Platform.constants` answers Android synchronously, so its model and manufacturer ride the
|
|
6544
|
+
* very first event. iOS exposes neither there — only the native bridge knows, and the bridge is
|
|
6545
|
+
* async and may not be installed at all. Without this the iOS half of every install reported no
|
|
6546
|
+
* model whatsoever.
|
|
6547
|
+
*
|
|
6548
|
+
* `buildEnvelope` reads `cfg.context` per event, so applying it here reaches every event from
|
|
6549
|
+
* the next one onward. Detected keys WIN over whatever the integrator configured, for the same
|
|
6550
|
+
* reason `mergeDeviceContext` inverts the usual precedence: a measurement must not be
|
|
6551
|
+
* overridable by a guess. Empty patches are ignored so a bridge that answered with nothing
|
|
6552
|
+
* cannot blank a value the static path already found.
|
|
6553
|
+
*/
|
|
6554
|
+
applyDetectedContext(patch) {
|
|
6555
|
+
if (!patch || Object.keys(patch).length === 0) return;
|
|
6556
|
+
this.cfg.context = {
|
|
6557
|
+
...this.cfg.context ?? {},
|
|
6558
|
+
...patch
|
|
6559
|
+
};
|
|
6560
|
+
}
|
|
6531
6561
|
// ─── internals ─────────────────────────────────────────────────────────────
|
|
6532
6562
|
buildEnvelope(eventName, properties) {
|
|
6533
6563
|
const ctx = this.cfg.context ?? {};
|
|
@@ -14198,6 +14228,7 @@ var SDKBootstrapper = class {
|
|
|
14198
14228
|
|
|
14199
14229
|
// lib/module/public/ScaleBunFacade.js
|
|
14200
14230
|
init_EventTracker();
|
|
14231
|
+
init_bridgeAdapter();
|
|
14201
14232
|
init_automaticEvents();
|
|
14202
14233
|
|
|
14203
14234
|
// lib/module/analytics/eventLane.js
|
|
@@ -16714,6 +16745,8 @@ var ScaleBunFacade = class {
|
|
|
16714
16745
|
* the whole time. The SDK knows the device; the integrator is guessing at it.
|
|
16715
16746
|
* Everything outside the detected keys is still theirs.
|
|
16716
16747
|
*/
|
|
16748
|
+
// Everything Platform.constants knows. On iOS that is neither the model nor the
|
|
16749
|
+
// manufacturer, so the bridge completes it below once it answers.
|
|
16717
16750
|
context: mergeDeviceContext(rawConfig?.context),
|
|
16718
16751
|
// Use the same foreground id as replay/journey capture. The
|
|
16719
16752
|
// callback is resolved per event because the session subsystem
|
|
@@ -16727,6 +16760,7 @@ var ScaleBunFacade = class {
|
|
|
16727
16760
|
});
|
|
16728
16761
|
this._eventTracker.start();
|
|
16729
16762
|
__DEV__ && logger.info("[ScaleBun] Envelope event tracking started \u2192 /v1/batch.");
|
|
16763
|
+
this._applyNativeDeviceContext(this._eventTracker);
|
|
16730
16764
|
this._captureInstallReferrer(this._eventTracker);
|
|
16731
16765
|
const tracker = this._eventTracker;
|
|
16732
16766
|
this._configManager = new ConfigManager({
|
|
@@ -16745,6 +16779,25 @@ var ScaleBunFacade = class {
|
|
|
16745
16779
|
logger.warn("[ScaleBun] Failed to start envelope event tracking:", err?.message);
|
|
16746
16780
|
}
|
|
16747
16781
|
}
|
|
16782
|
+
/**
|
|
16783
|
+
* Fold the native bridge's device facts into the analytics context once it answers.
|
|
16784
|
+
*
|
|
16785
|
+
* Detected values never overwrite a populated one with an empty one — `nativeDeviceContext`
|
|
16786
|
+
* drops absent keys and `applyDetectedContext` ignores an empty patch — so a bridge that
|
|
16787
|
+
* cannot answer leaves whatever `Platform.constants` already found.
|
|
16788
|
+
*/
|
|
16789
|
+
_applyNativeDeviceContext(tracker) {
|
|
16790
|
+
Promise.resolve().then(() => bridgeAdapter.getDeviceInfo()).then((info) => {
|
|
16791
|
+
if (!info) return;
|
|
16792
|
+
tracker.applyDetectedContext(nativeDeviceContext({
|
|
16793
|
+
osVersion: info.osVersion,
|
|
16794
|
+
deviceModel: info.deviceModel
|
|
16795
|
+
// iOS ships no manufacturer over the bridge; it is a constant there and the
|
|
16796
|
+
// static path already set it. Android's arrives via Platform.constants.
|
|
16797
|
+
}));
|
|
16798
|
+
}).catch(() => {
|
|
16799
|
+
});
|
|
16800
|
+
}
|
|
16748
16801
|
/**
|
|
16749
16802
|
* Android-only: read the Play Install Referrer once per install and route it into
|
|
16750
16803
|
* attribution. `scalebun_click_id` → the deterministic click path; the full
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The ONE place that answers "which iPhone is this?".
|
|
5
|
+
*
|
|
6
|
+
* iOS ships no API for the name a person would recognise. `UIDevice.current.model` returns the
|
|
7
|
+
* FAMILY — the literal string "iPhone" or "iPad" — which is identical on an iPhone 6 and an
|
|
8
|
+
* iPhone 17 Pro, so a dashboard grouping by it sees one bucket holding every iOS device ever.
|
|
9
|
+
* `ReplaySdk.getDeviceInfo` sent exactly that, which is why the replay lane could never tell two
|
|
10
|
+
* iPhones apart.
|
|
11
|
+
*
|
|
12
|
+
* The hardware identifier from `uname()` — "iPhone16,2" — is the only thing the OS will tell us,
|
|
13
|
+
* and it is unique per model. It is not the marketing name and this module does not pretend
|
|
14
|
+
* otherwise: turning "iPhone16,2" into "iPhone 15 Pro Max" needs a lookup table, and that table
|
|
15
|
+
* belongs on the server, where a device released after this binary shipped can be named without
|
|
16
|
+
* waiting for every app to adopt a new SDK and ship to the App Store.
|
|
17
|
+
*
|
|
18
|
+
* `ScaleBunProfilerModule` already had this function privately; both lanes now share it so they
|
|
19
|
+
* cannot drift into reporting two different models for one device.
|
|
20
|
+
*/
|
|
21
|
+
@objc(ScaleBunDeviceIdentity)
|
|
22
|
+
public final class ScaleBunDeviceIdentity: NSObject {
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The hardware identifier, e.g. "iPhone16,2" / "iPad14,3".
|
|
26
|
+
*
|
|
27
|
+
* On the simulator `uname` reports the HOST architecture ("x86_64", "arm64"), so the
|
|
28
|
+
* simulator's own hint is preferred when present — otherwise every developer's simulator
|
|
29
|
+
* session lands in the dashboard as a device model named after a CPU.
|
|
30
|
+
*/
|
|
31
|
+
@objc public static func modelIdentifier() -> String {
|
|
32
|
+
if let simulator = ProcessInfo.processInfo
|
|
33
|
+
.environment["SIMULATOR_MODEL_IDENTIFIER"], !simulator.isEmpty {
|
|
34
|
+
return simulator
|
|
35
|
+
}
|
|
36
|
+
var systemInfo = utsname()
|
|
37
|
+
uname(&systemInfo)
|
|
38
|
+
let identifier = withUnsafePointer(to: &systemInfo.machine) {
|
|
39
|
+
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
|
|
40
|
+
String(validatingUTF8: $0) ?? ""
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Empty rather than "Unknown": an absent measurement must not arrive looking like a value.
|
|
44
|
+
return identifier
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// No manufacturer constant here on purpose. iOS's manufacturer is answerable without the
|
|
48
|
+
// bridge — the platform being iOS entails it — so staticDeviceContext() sets it on the FIRST
|
|
49
|
+
// event rather than waiting for a native round-trip that may never come back.
|
|
50
|
+
}
|
|
@@ -392,15 +392,9 @@ class ScaleBunProfilerModule: RCTEventEmitter {
|
|
|
392
392
|
sendEvent(withName: "ScaleBunProfiler_Capabilities", body: buildCapabilities())
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
/** Shared with the replay lane so the two cannot report different models for one device. */
|
|
395
396
|
private func deviceModel() -> String {
|
|
396
|
-
|
|
397
|
-
uname(&systemInfo)
|
|
398
|
-
let modelCode = withUnsafePointer(to: &systemInfo.machine) {
|
|
399
|
-
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
|
|
400
|
-
String(validatingUTF8: $0) ?? "Unknown"
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return modelCode
|
|
397
|
+
return ScaleBunDeviceIdentity.modelIdentifier()
|
|
404
398
|
}
|
|
405
399
|
|
|
406
400
|
private func isDebugBuild() -> Bool {
|
package/ios/ReplaySdk.swift
CHANGED
|
@@ -572,7 +572,9 @@ class ReplaySdk: RCTEventEmitter {
|
|
|
572
572
|
resolve([
|
|
573
573
|
"platform": "ios",
|
|
574
574
|
"osVersion": device.systemVersion,
|
|
575
|
-
|
|
575
|
+
// NOT device.model — that is the family ("iPhone"), identical on every iPhone ever
|
|
576
|
+
// made. ScaleBunDeviceIdentity reports the hardware identifier, which is unique per model.
|
|
577
|
+
"deviceModel": ScaleBunDeviceIdentity.modelIdentifier(),
|
|
576
578
|
"screenWidth": Int(screen.bounds.width * screen.scale),
|
|
577
579
|
"screenHeight": Int(screen.bounds.height * screen.scale),
|
|
578
580
|
"pixelDensity": screen.scale,
|
|
@@ -341,6 +341,28 @@ class EventTracker {
|
|
|
341
341
|
this.persistQueue();
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Fold in device facts that were not knowable at init.
|
|
346
|
+
*
|
|
347
|
+
* `Platform.constants` answers Android synchronously, so its model and manufacturer ride the
|
|
348
|
+
* very first event. iOS exposes neither there — only the native bridge knows, and the bridge is
|
|
349
|
+
* async and may not be installed at all. Without this the iOS half of every install reported no
|
|
350
|
+
* model whatsoever.
|
|
351
|
+
*
|
|
352
|
+
* `buildEnvelope` reads `cfg.context` per event, so applying it here reaches every event from
|
|
353
|
+
* the next one onward. Detected keys WIN over whatever the integrator configured, for the same
|
|
354
|
+
* reason `mergeDeviceContext` inverts the usual precedence: a measurement must not be
|
|
355
|
+
* overridable by a guess. Empty patches are ignored so a bridge that answered with nothing
|
|
356
|
+
* cannot blank a value the static path already found.
|
|
357
|
+
*/
|
|
358
|
+
applyDetectedContext(patch) {
|
|
359
|
+
if (!patch || Object.keys(patch).length === 0) return;
|
|
360
|
+
this.cfg.context = {
|
|
361
|
+
...(this.cfg.context ?? {}),
|
|
362
|
+
...patch
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
344
366
|
// ─── internals ─────────────────────────────────────────────────────────────
|
|
345
367
|
|
|
346
368
|
buildEnvelope(eventName, properties) {
|
|
@@ -9,5 +9,5 @@ exports.SDK_VERSION = void 0;
|
|
|
9
9
|
* can attribute telemetry to the SDK build that produced it.
|
|
10
10
|
* Keep in sync with package.json "version".
|
|
11
11
|
*/
|
|
12
|
-
const SDK_VERSION = exports.SDK_VERSION = '1.10.
|
|
12
|
+
const SDK_VERSION = exports.SDK_VERSION = '1.10.6';
|
|
13
13
|
//# sourceMappingURL=version.js.map
|
|
@@ -104,6 +104,10 @@ function staticDeviceContext() {
|
|
|
104
104
|
if (osVersion) out.os_version = String(osVersion);
|
|
105
105
|
if (c.Model) out.device_model = String(c.Model);
|
|
106
106
|
if (c.Manufacturer) out.manufacturer = String(c.Manufacturer);else if (c.Brand) out.manufacturer = String(c.Brand);
|
|
107
|
+
// iOS exposes no manufacturer because it does not need to: every device running iOS is Apple's.
|
|
108
|
+
// A tautology, not a guess — and it means the field is populated on event #1 rather than waiting
|
|
109
|
+
// for a bridge that may never answer.
|
|
110
|
+
else if (out.platform_os === 'ios') out.manufacturer = 'Apple';
|
|
107
111
|
return out;
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -113,6 +117,7 @@ function nativeDeviceContext(info) {
|
|
|
113
117
|
if (!info) return out;
|
|
114
118
|
if (info.osVersion) out.os_version = info.osVersion;
|
|
115
119
|
if (info.deviceModel) out.device_model = info.deviceModel;
|
|
120
|
+
if (info.manufacturer) out.manufacturer = info.manufacturer;
|
|
116
121
|
return out;
|
|
117
122
|
}
|
|
118
123
|
|
|
@@ -20,6 +20,7 @@ var _debug = require("../debug");
|
|
|
20
20
|
var _api = require("../features/replay/public/api");
|
|
21
21
|
var _SDKBootstrapper = require("../bootstrap/SDKBootstrapper");
|
|
22
22
|
var _EventTracker = require("../analytics/EventTracker");
|
|
23
|
+
var _bridgeAdapter = require("../features/replay/bridge/adapters/bridgeAdapter");
|
|
23
24
|
var _automaticEvents = require("../analytics/automaticEvents");
|
|
24
25
|
var _eventLane = require("../analytics/eventLane");
|
|
25
26
|
var _ConfigManager = require("../config/ConfigManager");
|
|
@@ -313,6 +314,8 @@ class ScaleBunFacade {
|
|
|
313
314
|
* the whole time. The SDK knows the device; the integrator is guessing at it.
|
|
314
315
|
* Everything outside the detected keys is still theirs.
|
|
315
316
|
*/
|
|
317
|
+
// Everything Platform.constants knows. On iOS that is neither the model nor the
|
|
318
|
+
// manufacturer, so the bridge completes it below once it answers.
|
|
316
319
|
context: (0, _device.mergeDeviceContext)(rawConfig?.context),
|
|
317
320
|
// Use the same foreground id as replay/journey capture. The
|
|
318
321
|
// callback is resolved per event because the session subsystem
|
|
@@ -327,6 +330,19 @@ class ScaleBunFacade {
|
|
|
327
330
|
this._eventTracker.start();
|
|
328
331
|
__DEV__ && _internalLogger.logger.info('[ScaleBun] Envelope event tracking started → /v1/batch.');
|
|
329
332
|
|
|
333
|
+
// Complete the device context from the native bridge.
|
|
334
|
+
//
|
|
335
|
+
// `Platform.constants` answers Android synchronously — model and manufacturer ride the
|
|
336
|
+
// first event. It answers NEITHER on iOS, so without this every iOS installation
|
|
337
|
+
// reported no device model at all: `nativeDeviceContext` existed for exactly this and
|
|
338
|
+
// had no caller, and the bridge's own answer was `UIDevice.current.model` — the string
|
|
339
|
+
// "iPhone", identical on every iPhone ever made.
|
|
340
|
+
//
|
|
341
|
+
// Fire-and-forget on purpose. The bridge is async and absent entirely on Expo Go or an
|
|
342
|
+
// older native binary; waiting on it would delay the first event, and failing on it
|
|
343
|
+
// would take analytics down over a nice-to-have field.
|
|
344
|
+
this._applyNativeDeviceContext(this._eventTracker);
|
|
345
|
+
|
|
330
346
|
// Android: capture the Play Install Referrer once per install → feeds the
|
|
331
347
|
// deterministic click_id path + a one-off install_referrer event (UTM/gclid).
|
|
332
348
|
// Fire-and-forget; no-op off Android.
|
|
@@ -351,6 +367,24 @@ class ScaleBunFacade {
|
|
|
351
367
|
}
|
|
352
368
|
}
|
|
353
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Fold the native bridge's device facts into the analytics context once it answers.
|
|
372
|
+
*
|
|
373
|
+
* Detected values never overwrite a populated one with an empty one — `nativeDeviceContext`
|
|
374
|
+
* drops absent keys and `applyDetectedContext` ignores an empty patch — so a bridge that
|
|
375
|
+
* cannot answer leaves whatever `Platform.constants` already found.
|
|
376
|
+
*/
|
|
377
|
+
_applyNativeDeviceContext(tracker) {
|
|
378
|
+
Promise.resolve().then(() => _bridgeAdapter.bridgeAdapter.getDeviceInfo()).then(info => {
|
|
379
|
+
if (!info) return;
|
|
380
|
+
tracker.applyDetectedContext((0, _device.nativeDeviceContext)({
|
|
381
|
+
osVersion: info.osVersion,
|
|
382
|
+
deviceModel: info.deviceModel
|
|
383
|
+
// iOS ships no manufacturer over the bridge; it is a constant there and the
|
|
384
|
+
// static path already set it. Android's arrives via Platform.constants.
|
|
385
|
+
}));
|
|
386
|
+
}).catch(() => {/* bridge unavailable — the static context stands */});
|
|
387
|
+
}
|
|
354
388
|
/**
|
|
355
389
|
* Android-only: read the Play Install Referrer once per install and route it into
|
|
356
390
|
* attribution. `scalebun_click_id` → the deterministic click path; the full
|
|
@@ -336,6 +336,28 @@ export class EventTracker {
|
|
|
336
336
|
this.persistQueue();
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Fold in device facts that were not knowable at init.
|
|
341
|
+
*
|
|
342
|
+
* `Platform.constants` answers Android synchronously, so its model and manufacturer ride the
|
|
343
|
+
* very first event. iOS exposes neither there — only the native bridge knows, and the bridge is
|
|
344
|
+
* async and may not be installed at all. Without this the iOS half of every install reported no
|
|
345
|
+
* model whatsoever.
|
|
346
|
+
*
|
|
347
|
+
* `buildEnvelope` reads `cfg.context` per event, so applying it here reaches every event from
|
|
348
|
+
* the next one onward. Detected keys WIN over whatever the integrator configured, for the same
|
|
349
|
+
* reason `mergeDeviceContext` inverts the usual precedence: a measurement must not be
|
|
350
|
+
* overridable by a guess. Empty patches are ignored so a bridge that answered with nothing
|
|
351
|
+
* cannot blank a value the static path already found.
|
|
352
|
+
*/
|
|
353
|
+
applyDetectedContext(patch) {
|
|
354
|
+
if (!patch || Object.keys(patch).length === 0) return;
|
|
355
|
+
this.cfg.context = {
|
|
356
|
+
...(this.cfg.context ?? {}),
|
|
357
|
+
...patch
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
339
361
|
// ─── internals ─────────────────────────────────────────────────────────────
|
|
340
362
|
|
|
341
363
|
buildEnvelope(eventName, properties) {
|
|
@@ -92,6 +92,10 @@ export function staticDeviceContext() {
|
|
|
92
92
|
if (osVersion) out.os_version = String(osVersion);
|
|
93
93
|
if (c.Model) out.device_model = String(c.Model);
|
|
94
94
|
if (c.Manufacturer) out.manufacturer = String(c.Manufacturer);else if (c.Brand) out.manufacturer = String(c.Brand);
|
|
95
|
+
// iOS exposes no manufacturer because it does not need to: every device running iOS is Apple's.
|
|
96
|
+
// A tautology, not a guess — and it means the field is populated on event #1 rather than waiting
|
|
97
|
+
// for a bridge that may never answer.
|
|
98
|
+
else if (out.platform_os === 'ios') out.manufacturer = 'Apple';
|
|
95
99
|
return out;
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -101,6 +105,7 @@ export function nativeDeviceContext(info) {
|
|
|
101
105
|
if (!info) return out;
|
|
102
106
|
if (info.osVersion) out.os_version = info.osVersion;
|
|
103
107
|
if (info.deviceModel) out.device_model = info.deviceModel;
|
|
108
|
+
if (info.manufacturer) out.manufacturer = info.manufacturer;
|
|
104
109
|
return out;
|
|
105
110
|
}
|
|
106
111
|
|
|
@@ -18,13 +18,14 @@ import { BugReportFeature } from '../features/bugreport/BugReportFeature';
|
|
|
18
18
|
import { PersistentQueue } from '../pipeline/queue/persistentQueue';
|
|
19
19
|
import { MemoryBackend, createStorageBackend } from '../storage/StorageBackend';
|
|
20
20
|
import { getDeviceId } from '../core/id/deviceId';
|
|
21
|
-
import { mergeDeviceContext, resolveEventPlatform } from '../core/context/device';
|
|
21
|
+
import { mergeDeviceContext, nativeDeviceContext, resolveEventPlatform } from '../core/context/device';
|
|
22
22
|
import { coreContainer } from '../core/di/container';
|
|
23
23
|
import { flushQueue } from '../pipeline/queue/flushQueue';
|
|
24
24
|
import { enableDebug as _enableDebug, disableDebug as _disableDebug, isDebugEnabled, addBreadcrumb as _addBreadcrumb, ping as _ping, _getStream, _setFlushFn, getPerformanceFeature as _getPerformanceFeature, requestExport as _requestExport, buildDebugConfigFromInitConfig, applyFeatureGatesToDebugConfig, sendDebugMetric } from '../debug';
|
|
25
25
|
import { ReplaySdk } from '../features/replay/public/api';
|
|
26
26
|
import { SDKBootstrapper } from '../bootstrap/SDKBootstrapper';
|
|
27
27
|
import { EventTracker } from '../analytics/EventTracker';
|
|
28
|
+
import { bridgeAdapter } from '../features/replay/bridge/adapters/bridgeAdapter';
|
|
28
29
|
import { emitAutomaticEvent } from '../analytics/automaticEvents';
|
|
29
30
|
import { resolveEventLane, isUploadedSessionEvent } from '../analytics/eventLane';
|
|
30
31
|
import { ConfigManager } from '../config/ConfigManager';
|
|
@@ -307,6 +308,8 @@ class ScaleBunFacade {
|
|
|
307
308
|
* the whole time. The SDK knows the device; the integrator is guessing at it.
|
|
308
309
|
* Everything outside the detected keys is still theirs.
|
|
309
310
|
*/
|
|
311
|
+
// Everything Platform.constants knows. On iOS that is neither the model nor the
|
|
312
|
+
// manufacturer, so the bridge completes it below once it answers.
|
|
310
313
|
context: mergeDeviceContext(rawConfig?.context),
|
|
311
314
|
// Use the same foreground id as replay/journey capture. The
|
|
312
315
|
// callback is resolved per event because the session subsystem
|
|
@@ -321,6 +324,19 @@ class ScaleBunFacade {
|
|
|
321
324
|
this._eventTracker.start();
|
|
322
325
|
__DEV__ && logger.info('[ScaleBun] Envelope event tracking started → /v1/batch.');
|
|
323
326
|
|
|
327
|
+
// Complete the device context from the native bridge.
|
|
328
|
+
//
|
|
329
|
+
// `Platform.constants` answers Android synchronously — model and manufacturer ride the
|
|
330
|
+
// first event. It answers NEITHER on iOS, so without this every iOS installation
|
|
331
|
+
// reported no device model at all: `nativeDeviceContext` existed for exactly this and
|
|
332
|
+
// had no caller, and the bridge's own answer was `UIDevice.current.model` — the string
|
|
333
|
+
// "iPhone", identical on every iPhone ever made.
|
|
334
|
+
//
|
|
335
|
+
// Fire-and-forget on purpose. The bridge is async and absent entirely on Expo Go or an
|
|
336
|
+
// older native binary; waiting on it would delay the first event, and failing on it
|
|
337
|
+
// would take analytics down over a nice-to-have field.
|
|
338
|
+
this._applyNativeDeviceContext(this._eventTracker);
|
|
339
|
+
|
|
324
340
|
// Android: capture the Play Install Referrer once per install → feeds the
|
|
325
341
|
// deterministic click_id path + a one-off install_referrer event (UTM/gclid).
|
|
326
342
|
// Fire-and-forget; no-op off Android.
|
|
@@ -345,6 +361,24 @@ class ScaleBunFacade {
|
|
|
345
361
|
}
|
|
346
362
|
}
|
|
347
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Fold the native bridge's device facts into the analytics context once it answers.
|
|
366
|
+
*
|
|
367
|
+
* Detected values never overwrite a populated one with an empty one — `nativeDeviceContext`
|
|
368
|
+
* drops absent keys and `applyDetectedContext` ignores an empty patch — so a bridge that
|
|
369
|
+
* cannot answer leaves whatever `Platform.constants` already found.
|
|
370
|
+
*/
|
|
371
|
+
_applyNativeDeviceContext(tracker) {
|
|
372
|
+
Promise.resolve().then(() => bridgeAdapter.getDeviceInfo()).then(info => {
|
|
373
|
+
if (!info) return;
|
|
374
|
+
tracker.applyDetectedContext(nativeDeviceContext({
|
|
375
|
+
osVersion: info.osVersion,
|
|
376
|
+
deviceModel: info.deviceModel
|
|
377
|
+
// iOS ships no manufacturer over the bridge; it is a constant there and the
|
|
378
|
+
// static path already set it. Android's arrives via Platform.constants.
|
|
379
|
+
}));
|
|
380
|
+
}).catch(() => {/* bridge unavailable — the static context stands */});
|
|
381
|
+
}
|
|
348
382
|
/**
|
|
349
383
|
* Android-only: read the Play Install Referrer once per install and route it into
|
|
350
384
|
* attribution. `scalebun_click_id` → the deterministic click path; the full
|
|
@@ -134,6 +134,21 @@ export declare class EventTracker {
|
|
|
134
134
|
}): void;
|
|
135
135
|
flush(): Promise<void>;
|
|
136
136
|
stop(): void;
|
|
137
|
+
/**
|
|
138
|
+
* Fold in device facts that were not knowable at init.
|
|
139
|
+
*
|
|
140
|
+
* `Platform.constants` answers Android synchronously, so its model and manufacturer ride the
|
|
141
|
+
* very first event. iOS exposes neither there — only the native bridge knows, and the bridge is
|
|
142
|
+
* async and may not be installed at all. Without this the iOS half of every install reported no
|
|
143
|
+
* model whatsoever.
|
|
144
|
+
*
|
|
145
|
+
* `buildEnvelope` reads `cfg.context` per event, so applying it here reaches every event from
|
|
146
|
+
* the next one onward. Detected keys WIN over whatever the integrator configured, for the same
|
|
147
|
+
* reason `mergeDeviceContext` inverts the usual precedence: a measurement must not be
|
|
148
|
+
* overridable by a guess. Empty patches are ignored so a bridge that answered with nothing
|
|
149
|
+
* cannot blank a value the static path already found.
|
|
150
|
+
*/
|
|
151
|
+
applyDetectedContext(patch: Record<string, string>): void;
|
|
137
152
|
private buildEnvelope;
|
|
138
153
|
private enqueue;
|
|
139
154
|
private scheduleFlush;
|
|
@@ -34,6 +34,7 @@ export declare function nativeDeviceContext(info: {
|
|
|
34
34
|
platform?: string;
|
|
35
35
|
osVersion?: string;
|
|
36
36
|
deviceModel?: string;
|
|
37
|
+
manufacturer?: string;
|
|
37
38
|
} | null): Record<string, string>;
|
|
38
39
|
/**
|
|
39
40
|
* Merge the integrator's context with what we detected — DETECTED WINS on the keys above.
|
|
@@ -80,6 +80,14 @@ declare class ScaleBunFacade {
|
|
|
80
80
|
private _autoEnableDebug;
|
|
81
81
|
/** Boot the Phase 1 envelope tracking lane if an appId is configured. */
|
|
82
82
|
private _maybeStartEventTracker;
|
|
83
|
+
/**
|
|
84
|
+
* Fold the native bridge's device facts into the analytics context once it answers.
|
|
85
|
+
*
|
|
86
|
+
* Detected values never overwrite a populated one with an empty one — `nativeDeviceContext`
|
|
87
|
+
* drops absent keys and `applyDetectedContext` ignores an empty patch — so a bridge that
|
|
88
|
+
* cannot answer leaves whatever `Platform.constants` already found.
|
|
89
|
+
*/
|
|
90
|
+
private _applyNativeDeviceContext;
|
|
83
91
|
/**
|
|
84
92
|
* Android-only: read the Play Install Referrer once per install and route it into
|
|
85
93
|
* attribution. `scalebun_click_id` → the deterministic click path; the full
|
package/package.json
CHANGED
|
@@ -387,6 +387,25 @@ export class EventTracker {
|
|
|
387
387
|
this.persistQueue();
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Fold in device facts that were not knowable at init.
|
|
392
|
+
*
|
|
393
|
+
* `Platform.constants` answers Android synchronously, so its model and manufacturer ride the
|
|
394
|
+
* very first event. iOS exposes neither there — only the native bridge knows, and the bridge is
|
|
395
|
+
* async and may not be installed at all. Without this the iOS half of every install reported no
|
|
396
|
+
* model whatsoever.
|
|
397
|
+
*
|
|
398
|
+
* `buildEnvelope` reads `cfg.context` per event, so applying it here reaches every event from
|
|
399
|
+
* the next one onward. Detected keys WIN over whatever the integrator configured, for the same
|
|
400
|
+
* reason `mergeDeviceContext` inverts the usual precedence: a measurement must not be
|
|
401
|
+
* overridable by a guess. Empty patches are ignored so a bridge that answered with nothing
|
|
402
|
+
* cannot blank a value the static path already found.
|
|
403
|
+
*/
|
|
404
|
+
applyDetectedContext(patch: Record<string, string>): void {
|
|
405
|
+
if (!patch || Object.keys(patch).length === 0) return;
|
|
406
|
+
this.cfg.context = { ...(this.cfg.context ?? {}), ...patch };
|
|
407
|
+
}
|
|
408
|
+
|
|
390
409
|
// ─── internals ─────────────────────────────────────────────────────────────
|
|
391
410
|
|
|
392
411
|
private buildEnvelope(eventName: string, properties?: Record<string, any>): Envelope {
|
|
@@ -114,6 +114,10 @@ export function staticDeviceContext(): Record<string, string> {
|
|
|
114
114
|
if (c.Model) out.device_model = String(c.Model);
|
|
115
115
|
if (c.Manufacturer) out.manufacturer = String(c.Manufacturer);
|
|
116
116
|
else if (c.Brand) out.manufacturer = String(c.Brand);
|
|
117
|
+
// iOS exposes no manufacturer because it does not need to: every device running iOS is Apple's.
|
|
118
|
+
// A tautology, not a guess — and it means the field is populated on event #1 rather than waiting
|
|
119
|
+
// for a bridge that may never answer.
|
|
120
|
+
else if (out.platform_os === 'ios') out.manufacturer = 'Apple';
|
|
117
121
|
return out;
|
|
118
122
|
}
|
|
119
123
|
|
|
@@ -122,11 +126,13 @@ export function nativeDeviceContext(info: {
|
|
|
122
126
|
platform?: string;
|
|
123
127
|
osVersion?: string;
|
|
124
128
|
deviceModel?: string;
|
|
129
|
+
manufacturer?: string;
|
|
125
130
|
} | null): Record<string, string> {
|
|
126
131
|
const out: Record<string, string> = {};
|
|
127
132
|
if (!info) return out;
|
|
128
133
|
if (info.osVersion) out.os_version = info.osVersion;
|
|
129
134
|
if (info.deviceModel) out.device_model = info.deviceModel;
|
|
135
|
+
if (info.manufacturer) out.manufacturer = info.manufacturer;
|
|
130
136
|
return out;
|
|
131
137
|
}
|
|
132
138
|
|
|
@@ -20,7 +20,7 @@ import { PersistentQueue } from '../pipeline/queue/persistentQueue';
|
|
|
20
20
|
import { MemoryBackend, createStorageBackend } from '../storage/StorageBackend';
|
|
21
21
|
import { HttpClient } from '../transport/http/httpClient';
|
|
22
22
|
import { getDeviceId } from '../core/id/deviceId';
|
|
23
|
-
import { mergeDeviceContext, resolveEventPlatform } from '../core/context/device';
|
|
23
|
+
import { mergeDeviceContext, nativeDeviceContext, resolveEventPlatform } from '../core/context/device';
|
|
24
24
|
import { coreContainer } from '../core/di/container';
|
|
25
25
|
import { flushQueue } from '../pipeline/queue/flushQueue';
|
|
26
26
|
import {
|
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
} from '../features/replay/public/types';
|
|
49
49
|
import { SDKBootstrapper } from '../bootstrap/SDKBootstrapper';
|
|
50
50
|
import { EventTracker } from '../analytics/EventTracker';
|
|
51
|
+
import { bridgeAdapter } from '../features/replay/bridge/adapters/bridgeAdapter';
|
|
51
52
|
import {
|
|
52
53
|
emitAutomaticEvent,
|
|
53
54
|
type AutomaticEventName,
|
|
@@ -368,6 +369,8 @@ class ScaleBunFacade {
|
|
|
368
369
|
* the whole time. The SDK knows the device; the integrator is guessing at it.
|
|
369
370
|
* Everything outside the detected keys is still theirs.
|
|
370
371
|
*/
|
|
372
|
+
// Everything Platform.constants knows. On iOS that is neither the model nor the
|
|
373
|
+
// manufacturer, so the bridge completes it below once it answers.
|
|
371
374
|
context: mergeDeviceContext(rawConfig?.context),
|
|
372
375
|
// Use the same foreground id as replay/journey capture. The
|
|
373
376
|
// callback is resolved per event because the session subsystem
|
|
@@ -384,6 +387,19 @@ class ScaleBunFacade {
|
|
|
384
387
|
this._eventTracker.start();
|
|
385
388
|
__DEV__ && logger.info('[ScaleBun] Envelope event tracking started → /v1/batch.');
|
|
386
389
|
|
|
390
|
+
// Complete the device context from the native bridge.
|
|
391
|
+
//
|
|
392
|
+
// `Platform.constants` answers Android synchronously — model and manufacturer ride the
|
|
393
|
+
// first event. It answers NEITHER on iOS, so without this every iOS installation
|
|
394
|
+
// reported no device model at all: `nativeDeviceContext` existed for exactly this and
|
|
395
|
+
// had no caller, and the bridge's own answer was `UIDevice.current.model` — the string
|
|
396
|
+
// "iPhone", identical on every iPhone ever made.
|
|
397
|
+
//
|
|
398
|
+
// Fire-and-forget on purpose. The bridge is async and absent entirely on Expo Go or an
|
|
399
|
+
// older native binary; waiting on it would delay the first event, and failing on it
|
|
400
|
+
// would take analytics down over a nice-to-have field.
|
|
401
|
+
this._applyNativeDeviceContext(this._eventTracker);
|
|
402
|
+
|
|
387
403
|
// Android: capture the Play Install Referrer once per install → feeds the
|
|
388
404
|
// deterministic click_id path + a one-off install_referrer event (UTM/gclid).
|
|
389
405
|
// Fire-and-forget; no-op off Android.
|
|
@@ -405,6 +421,30 @@ class ScaleBunFacade {
|
|
|
405
421
|
}
|
|
406
422
|
}
|
|
407
423
|
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Fold the native bridge's device facts into the analytics context once it answers.
|
|
427
|
+
*
|
|
428
|
+
* Detected values never overwrite a populated one with an empty one — `nativeDeviceContext`
|
|
429
|
+
* drops absent keys and `applyDetectedContext` ignores an empty patch — so a bridge that
|
|
430
|
+
* cannot answer leaves whatever `Platform.constants` already found.
|
|
431
|
+
*/
|
|
432
|
+
private _applyNativeDeviceContext(tracker: EventTracker): void {
|
|
433
|
+
Promise.resolve()
|
|
434
|
+
.then(() => bridgeAdapter.getDeviceInfo())
|
|
435
|
+
.then(info => {
|
|
436
|
+
if (!info) return;
|
|
437
|
+
tracker.applyDetectedContext(
|
|
438
|
+
nativeDeviceContext({
|
|
439
|
+
osVersion: info.osVersion,
|
|
440
|
+
deviceModel: info.deviceModel,
|
|
441
|
+
// iOS ships no manufacturer over the bridge; it is a constant there and the
|
|
442
|
+
// static path already set it. Android's arrives via Platform.constants.
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
})
|
|
446
|
+
.catch(() => { /* bridge unavailable — the static context stands */ });
|
|
447
|
+
}
|
|
408
448
|
/**
|
|
409
449
|
* Android-only: read the Play Install Referrer once per install and route it into
|
|
410
450
|
* attribution. `scalebun_click_id` → the deterministic click path; the full
|