@layers/amba-react-native 4.0.14 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +183 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +56 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -95,6 +95,9 @@ var AmbaHttpClient = class {
|
|
|
95
95
|
baseUrl;
|
|
96
96
|
sdkPlatform;
|
|
97
97
|
sdkVersionHeader;
|
|
98
|
+
appVersion;
|
|
99
|
+
deviceRegion;
|
|
100
|
+
deviceOs;
|
|
98
101
|
identity;
|
|
99
102
|
fetchImpl;
|
|
100
103
|
ensureSession;
|
|
@@ -104,6 +107,9 @@ var AmbaHttpClient = class {
|
|
|
104
107
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
105
108
|
this.sdkPlatform = opts.sdkPlatform;
|
|
106
109
|
this.sdkVersionHeader = opts.sdkVersionHeader;
|
|
110
|
+
this.appVersion = opts.appVersion;
|
|
111
|
+
this.deviceRegion = opts.deviceRegion;
|
|
112
|
+
this.deviceOs = opts.deviceOs;
|
|
107
113
|
this.identity = opts.identity;
|
|
108
114
|
this.ensureSession = opts.ensureSession;
|
|
109
115
|
this.onSessionRefused = opts.onSessionRefused;
|
|
@@ -132,6 +138,9 @@ var AmbaHttpClient = class {
|
|
|
132
138
|
Accept: "application/json",
|
|
133
139
|
...extra
|
|
134
140
|
};
|
|
141
|
+
if (this.appVersion) h["X-App-Version"] = this.appVersion;
|
|
142
|
+
if (this.deviceRegion) h["X-Device-Region"] = this.deviceRegion;
|
|
143
|
+
if (this.deviceOs) h["X-Device-OS"] = this.deviceOs;
|
|
135
144
|
const tok = this.identity.getSessionToken();
|
|
136
145
|
if (tok) h.Authorization = `Bearer ${tok}`;
|
|
137
146
|
return h;
|
|
@@ -1757,6 +1766,117 @@ const notifications = {
|
|
|
1757
1766
|
}
|
|
1758
1767
|
};
|
|
1759
1768
|
|
|
1769
|
+
//#endregion
|
|
1770
|
+
//#region src/experiment-reporter.ts
|
|
1771
|
+
/** Keys remembered before the oldest are forgotten (bounds memory). */
|
|
1772
|
+
const DEFAULT_MAX_REPORTED = 1e4;
|
|
1773
|
+
var ExperimentAssignmentReporter = class {
|
|
1774
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1775
|
+
reported = /* @__PURE__ */ new Set();
|
|
1776
|
+
constructor(maxReported = DEFAULT_MAX_REPORTED) {
|
|
1777
|
+
this.maxReported = maxReported;
|
|
1778
|
+
}
|
|
1779
|
+
/** Add a listener. Returns a function that removes it. */
|
|
1780
|
+
subscribe(listener) {
|
|
1781
|
+
if (typeof listener !== "function") throw new TypeError("onExperimentAssigned expects a function");
|
|
1782
|
+
this.listeners.add(listener);
|
|
1783
|
+
return () => {
|
|
1784
|
+
this.listeners.delete(listener);
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Call the listeners for `assignment` when this user has not reported this
|
|
1789
|
+
* experiment yet in the session. Returns `assignment` unchanged.
|
|
1790
|
+
*/
|
|
1791
|
+
report(assignment) {
|
|
1792
|
+
if (this.listeners.size === 0) return assignment;
|
|
1793
|
+
const id = `${assignment.bucketingId}\u0000${assignment.experimentKey}`;
|
|
1794
|
+
if (this.reported.has(id)) {
|
|
1795
|
+
this.reported.delete(id);
|
|
1796
|
+
this.reported.add(id);
|
|
1797
|
+
return assignment;
|
|
1798
|
+
}
|
|
1799
|
+
this.reported.add(id);
|
|
1800
|
+
if (this.reported.size > this.maxReported) {
|
|
1801
|
+
const oldest = this.reported.values().next().value;
|
|
1802
|
+
if (oldest !== void 0) this.reported.delete(oldest);
|
|
1803
|
+
}
|
|
1804
|
+
for (const listener of Array.from(this.listeners)) try {
|
|
1805
|
+
const result = listener({ ...assignment });
|
|
1806
|
+
if (result && typeof result.then === "function") result.then(void 0, logListenerError);
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
logListenerError(err);
|
|
1809
|
+
}
|
|
1810
|
+
return assignment;
|
|
1811
|
+
}
|
|
1812
|
+
/** Start a new session: every experiment reports again. */
|
|
1813
|
+
reset() {
|
|
1814
|
+
this.reported.clear();
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
function logListenerError(err) {
|
|
1818
|
+
console.warn("[amba] onExperimentAssigned listener threw", err);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
//#endregion
|
|
1822
|
+
//#region src/client-info.ts
|
|
1823
|
+
/**
|
|
1824
|
+
* Values for the `X-App-Version`, `X-Device-Region` and `X-Device-OS`
|
|
1825
|
+
* request headers. Each is omitted when unknown, so every helper here
|
|
1826
|
+
* returns `undefined` for a value the header can't carry as-is.
|
|
1827
|
+
*/
|
|
1828
|
+
/** Longest `X-App-Version` value sent (matches the core's limit). */
|
|
1829
|
+
const MAX_APP_VERSION_LENGTH = 64;
|
|
1830
|
+
/**
|
|
1831
|
+
* An app version for `X-App-Version`: the text before the first whitespace
|
|
1832
|
+
* (`"2.3.1 (45)"` sends `2.3.1`), which must start with an ASCII letter or
|
|
1833
|
+
* digit, hold only letters, digits and `.` `_` `+` `-`, and be at most
|
|
1834
|
+
* {@link MAX_APP_VERSION_LENGTH} characters: the shape the API records for
|
|
1835
|
+
* segment targeting. Anything else returns `undefined`.
|
|
1836
|
+
*/
|
|
1837
|
+
function normalizeAppVersion(raw) {
|
|
1838
|
+
if (typeof raw !== "string") return void 0;
|
|
1839
|
+
const v = raw.trim().split(/\s+/)[0] ?? "";
|
|
1840
|
+
if (v.length === 0 || v.length > MAX_APP_VERSION_LENGTH) return void 0;
|
|
1841
|
+
return /^[0-9A-Za-z][0-9A-Za-z._+-]*$/.test(v) ? v : void 0;
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* The region subtag of a BCP 47 locale tag (`en-US` → `US`, `zh-Hant-TW` →
|
|
1845
|
+
* `TW`, POSIX `fr_FR.UTF-8` → `FR`, iOS `en_US@rg=gbzzzz` → `US`),
|
|
1846
|
+
* uppercased. Returns `undefined` when the tag carries no two-letter region
|
|
1847
|
+
* (`en`, `es-419`, `sr-Latn`).
|
|
1848
|
+
*/
|
|
1849
|
+
function regionFromLocale(tag) {
|
|
1850
|
+
if (typeof tag !== "string" || tag.length === 0) return void 0;
|
|
1851
|
+
const parts = tag.split(/[.@]/)[0].split(/[-_]/);
|
|
1852
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1853
|
+
const part = parts[i];
|
|
1854
|
+
if (part.length === 1) break;
|
|
1855
|
+
if (/^[A-Za-z]{2}$/.test(part)) return part.toUpperCase();
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
const DEVICE_OS_VALUES = [
|
|
1859
|
+
"ios",
|
|
1860
|
+
"android",
|
|
1861
|
+
"web",
|
|
1862
|
+
"macos",
|
|
1863
|
+
"windows",
|
|
1864
|
+
"linux",
|
|
1865
|
+
"other"
|
|
1866
|
+
];
|
|
1867
|
+
/**
|
|
1868
|
+
* Map a platform OS name (React Native's `Platform.OS`, for example) to an
|
|
1869
|
+
* `X-Device-OS` value. Listed names pass through lowercased; any other
|
|
1870
|
+
* non-empty name is a known-but-unlisted OS and maps to `"other"`. Empty or
|
|
1871
|
+
* missing returns `undefined`.
|
|
1872
|
+
*/
|
|
1873
|
+
function deviceOsFromPlatform(os) {
|
|
1874
|
+
if (typeof os !== "string") return void 0;
|
|
1875
|
+
const v = os.trim().toLowerCase();
|
|
1876
|
+
if (v.length === 0) return void 0;
|
|
1877
|
+
return DEVICE_OS_VALUES.includes(v) ? v : "other";
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1760
1880
|
//#endregion
|
|
1761
1881
|
//#region src/realtime.ts
|
|
1762
1882
|
const INITIAL_BACKOFF_MS = 500;
|
|
@@ -2080,7 +2200,7 @@ function delay(ms) {
|
|
|
2080
2200
|
//#endregion
|
|
2081
2201
|
//#region src/index.ts
|
|
2082
2202
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
2083
|
-
const SDK_VERSION = "4.0
|
|
2203
|
+
const SDK_VERSION = "4.1.0";
|
|
2084
2204
|
/**
|
|
2085
2205
|
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
2086
2206
|
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
@@ -2117,6 +2237,13 @@ function realtimeAuthHeaders() {
|
|
|
2117
2237
|
};
|
|
2118
2238
|
}
|
|
2119
2239
|
const authSubscribers = /* @__PURE__ */ new Set();
|
|
2240
|
+
/**
|
|
2241
|
+
* `Amba.onExperimentAssigned` listeners. `experiments.getVariant` reports
|
|
2242
|
+
* each result here; the reporter calls the listeners once per experiment per
|
|
2243
|
+
* user session. A sign-out (any auth change to no session) and `configure`
|
|
2244
|
+
* start a new session.
|
|
2245
|
+
*/
|
|
2246
|
+
const experimentReporter = new ExperimentAssignmentReporter();
|
|
2120
2247
|
function snapshotSession(user) {
|
|
2121
2248
|
if (!user) return null;
|
|
2122
2249
|
return {
|
|
@@ -2127,6 +2254,7 @@ function snapshotSession(user) {
|
|
|
2127
2254
|
};
|
|
2128
2255
|
}
|
|
2129
2256
|
function notifyAuthSubscribers(session) {
|
|
2257
|
+
if (session === null) experimentReporter.reset();
|
|
2130
2258
|
for (const cb of authSubscribers) try {
|
|
2131
2259
|
cb(session);
|
|
2132
2260
|
} catch (err) {
|
|
@@ -2543,6 +2671,7 @@ const P = {
|
|
|
2543
2671
|
aiAnthropic: "/v1/client/ai/anthropic/messages",
|
|
2544
2672
|
config: "/v1/client/config",
|
|
2545
2673
|
flags: "/v1/client/flags",
|
|
2674
|
+
experiments: "/v1/client/experiments",
|
|
2546
2675
|
diagnosticsPing: "/v1/client/diagnostics/ping",
|
|
2547
2676
|
achievements: "/v1/client/achievements",
|
|
2548
2677
|
challenges: "/v1/client/challenges",
|
|
@@ -2686,6 +2815,39 @@ async function deleteDirect(s, name, id) {
|
|
|
2686
2815
|
return await s.http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
|
|
2687
2816
|
}
|
|
2688
2817
|
/**
|
|
2818
|
+
* The device OS for `X-Device-OS` from React Native's `Platform.OS`
|
|
2819
|
+
* (`ios`, `android`, `web` for react-native-web, `macos`, `windows`).
|
|
2820
|
+
* `undefined` outside React Native (Node, tests).
|
|
2821
|
+
*/
|
|
2822
|
+
function detectDeviceOs() {
|
|
2823
|
+
try {
|
|
2824
|
+
const os = require("react-native").Platform?.OS;
|
|
2825
|
+
return deviceOsFromPlatform(typeof os === "string" ? os : void 0);
|
|
2826
|
+
} catch {
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* The device locale's region for `X-Device-Region`: the iOS region setting
|
|
2832
|
+
* (`AppleLocale`) or Android's locale, falling back to the JS runtime's
|
|
2833
|
+
* default locale. `undefined` when none carries a two-letter region.
|
|
2834
|
+
*/
|
|
2835
|
+
function detectDeviceRegion() {
|
|
2836
|
+
try {
|
|
2837
|
+
const rn = require("react-native");
|
|
2838
|
+
const ios = (rn.NativeModules?.["SettingsManager"])?.settings;
|
|
2839
|
+
const android = rn.NativeModules?.["I18nManager"];
|
|
2840
|
+
const tag = ios?.AppleLocale ?? android?.localeIdentifier;
|
|
2841
|
+
const region = regionFromLocale(typeof tag === "string" ? tag : void 0);
|
|
2842
|
+
if (region) return region;
|
|
2843
|
+
} catch {}
|
|
2844
|
+
try {
|
|
2845
|
+
return regionFromLocale(Intl.DateTimeFormat().resolvedOptions().locale);
|
|
2846
|
+
} catch {
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2689
2851
|
* Top-level SDK namespace for React Native.
|
|
2690
2852
|
*/
|
|
2691
2853
|
const Amba = {
|
|
@@ -2717,6 +2879,9 @@ const Amba = {
|
|
|
2717
2879
|
baseUrl,
|
|
2718
2880
|
sdkPlatform: "react-native",
|
|
2719
2881
|
sdkVersionHeader,
|
|
2882
|
+
appVersion: normalizeAppVersion(config.appVersion),
|
|
2883
|
+
deviceRegion: detectDeviceRegion(),
|
|
2884
|
+
deviceOs: detectDeviceOs(),
|
|
2720
2885
|
identity,
|
|
2721
2886
|
ensureSession: config.autoAnonymousSession === false ? void 0 : async (path) => {
|
|
2722
2887
|
if (pathSkipsLazySession(path)) return;
|
|
@@ -2729,6 +2894,7 @@ const Amba = {
|
|
|
2729
2894
|
if (prior?.offlineQueue) await prior.offlineQueue.dispose();
|
|
2730
2895
|
prior?.detachEventAppState?.();
|
|
2731
2896
|
if (prior?.eventQueue) await prior.eventQueue.dispose();
|
|
2897
|
+
experimentReporter.reset();
|
|
2732
2898
|
const nextState = {
|
|
2733
2899
|
http,
|
|
2734
2900
|
identity,
|
|
@@ -3420,6 +3586,22 @@ const Amba = {
|
|
|
3420
3586
|
}
|
|
3421
3587
|
}
|
|
3422
3588
|
},
|
|
3589
|
+
experiments: {
|
|
3590
|
+
getVariant: async (key) => {
|
|
3591
|
+
const s = getState();
|
|
3592
|
+
const userBefore = s.identity.getAppUserId();
|
|
3593
|
+
const env = await s.http.get(`${P.experiments}/${encodePathSegment(key)}/assignment`);
|
|
3594
|
+
return experimentReporter.report({
|
|
3595
|
+
experimentKey: env.data.experiment_key,
|
|
3596
|
+
variant: env.data.variant,
|
|
3597
|
+
bucketingId: userBefore ?? s.identity.getAppUserId() ?? ""
|
|
3598
|
+
});
|
|
3599
|
+
},
|
|
3600
|
+
trackExposure: async (key) => {
|
|
3601
|
+
await getState().http.post(`${P.experiments}/${encodePathSegment(key)}/exposure`, {});
|
|
3602
|
+
}
|
|
3603
|
+
},
|
|
3604
|
+
onExperimentAssigned: (listener) => experimentReporter.subscribe(listener),
|
|
3423
3605
|
diagnostics: { ping: async () => {
|
|
3424
3606
|
return await getState().http.post(P.diagnosticsPing, {});
|
|
3425
3607
|
} },
|