@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.js
CHANGED
|
@@ -101,6 +101,9 @@ var AmbaHttpClient = class {
|
|
|
101
101
|
baseUrl;
|
|
102
102
|
sdkPlatform;
|
|
103
103
|
sdkVersionHeader;
|
|
104
|
+
appVersion;
|
|
105
|
+
deviceRegion;
|
|
106
|
+
deviceOs;
|
|
104
107
|
identity;
|
|
105
108
|
fetchImpl;
|
|
106
109
|
ensureSession;
|
|
@@ -110,6 +113,9 @@ var AmbaHttpClient = class {
|
|
|
110
113
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
111
114
|
this.sdkPlatform = opts.sdkPlatform;
|
|
112
115
|
this.sdkVersionHeader = opts.sdkVersionHeader;
|
|
116
|
+
this.appVersion = opts.appVersion;
|
|
117
|
+
this.deviceRegion = opts.deviceRegion;
|
|
118
|
+
this.deviceOs = opts.deviceOs;
|
|
113
119
|
this.identity = opts.identity;
|
|
114
120
|
this.ensureSession = opts.ensureSession;
|
|
115
121
|
this.onSessionRefused = opts.onSessionRefused;
|
|
@@ -138,6 +144,9 @@ var AmbaHttpClient = class {
|
|
|
138
144
|
Accept: "application/json",
|
|
139
145
|
...extra
|
|
140
146
|
};
|
|
147
|
+
if (this.appVersion) h["X-App-Version"] = this.appVersion;
|
|
148
|
+
if (this.deviceRegion) h["X-Device-Region"] = this.deviceRegion;
|
|
149
|
+
if (this.deviceOs) h["X-Device-OS"] = this.deviceOs;
|
|
141
150
|
const tok = this.identity.getSessionToken();
|
|
142
151
|
if (tok) h.Authorization = `Bearer ${tok}`;
|
|
143
152
|
return h;
|
|
@@ -1763,6 +1772,117 @@ const notifications = {
|
|
|
1763
1772
|
}
|
|
1764
1773
|
};
|
|
1765
1774
|
|
|
1775
|
+
//#endregion
|
|
1776
|
+
//#region src/experiment-reporter.ts
|
|
1777
|
+
/** Keys remembered before the oldest are forgotten (bounds memory). */
|
|
1778
|
+
const DEFAULT_MAX_REPORTED = 1e4;
|
|
1779
|
+
var ExperimentAssignmentReporter = class {
|
|
1780
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1781
|
+
reported = /* @__PURE__ */ new Set();
|
|
1782
|
+
constructor(maxReported = DEFAULT_MAX_REPORTED) {
|
|
1783
|
+
this.maxReported = maxReported;
|
|
1784
|
+
}
|
|
1785
|
+
/** Add a listener. Returns a function that removes it. */
|
|
1786
|
+
subscribe(listener) {
|
|
1787
|
+
if (typeof listener !== "function") throw new TypeError("onExperimentAssigned expects a function");
|
|
1788
|
+
this.listeners.add(listener);
|
|
1789
|
+
return () => {
|
|
1790
|
+
this.listeners.delete(listener);
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Call the listeners for `assignment` when this user has not reported this
|
|
1795
|
+
* experiment yet in the session. Returns `assignment` unchanged.
|
|
1796
|
+
*/
|
|
1797
|
+
report(assignment) {
|
|
1798
|
+
if (this.listeners.size === 0) return assignment;
|
|
1799
|
+
const id = `${assignment.bucketingId}\u0000${assignment.experimentKey}`;
|
|
1800
|
+
if (this.reported.has(id)) {
|
|
1801
|
+
this.reported.delete(id);
|
|
1802
|
+
this.reported.add(id);
|
|
1803
|
+
return assignment;
|
|
1804
|
+
}
|
|
1805
|
+
this.reported.add(id);
|
|
1806
|
+
if (this.reported.size > this.maxReported) {
|
|
1807
|
+
const oldest = this.reported.values().next().value;
|
|
1808
|
+
if (oldest !== void 0) this.reported.delete(oldest);
|
|
1809
|
+
}
|
|
1810
|
+
for (const listener of Array.from(this.listeners)) try {
|
|
1811
|
+
const result = listener({ ...assignment });
|
|
1812
|
+
if (result && typeof result.then === "function") result.then(void 0, logListenerError);
|
|
1813
|
+
} catch (err) {
|
|
1814
|
+
logListenerError(err);
|
|
1815
|
+
}
|
|
1816
|
+
return assignment;
|
|
1817
|
+
}
|
|
1818
|
+
/** Start a new session: every experiment reports again. */
|
|
1819
|
+
reset() {
|
|
1820
|
+
this.reported.clear();
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
function logListenerError(err) {
|
|
1824
|
+
console.warn("[amba] onExperimentAssigned listener threw", err);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
//#endregion
|
|
1828
|
+
//#region src/client-info.ts
|
|
1829
|
+
/**
|
|
1830
|
+
* Values for the `X-App-Version`, `X-Device-Region` and `X-Device-OS`
|
|
1831
|
+
* request headers. Each is omitted when unknown, so every helper here
|
|
1832
|
+
* returns `undefined` for a value the header can't carry as-is.
|
|
1833
|
+
*/
|
|
1834
|
+
/** Longest `X-App-Version` value sent (matches the core's limit). */
|
|
1835
|
+
const MAX_APP_VERSION_LENGTH = 64;
|
|
1836
|
+
/**
|
|
1837
|
+
* An app version for `X-App-Version`: the text before the first whitespace
|
|
1838
|
+
* (`"2.3.1 (45)"` sends `2.3.1`), which must start with an ASCII letter or
|
|
1839
|
+
* digit, hold only letters, digits and `.` `_` `+` `-`, and be at most
|
|
1840
|
+
* {@link MAX_APP_VERSION_LENGTH} characters: the shape the API records for
|
|
1841
|
+
* segment targeting. Anything else returns `undefined`.
|
|
1842
|
+
*/
|
|
1843
|
+
function normalizeAppVersion(raw) {
|
|
1844
|
+
if (typeof raw !== "string") return void 0;
|
|
1845
|
+
const v = raw.trim().split(/\s+/)[0] ?? "";
|
|
1846
|
+
if (v.length === 0 || v.length > MAX_APP_VERSION_LENGTH) return void 0;
|
|
1847
|
+
return /^[0-9A-Za-z][0-9A-Za-z._+-]*$/.test(v) ? v : void 0;
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* The region subtag of a BCP 47 locale tag (`en-US` → `US`, `zh-Hant-TW` →
|
|
1851
|
+
* `TW`, POSIX `fr_FR.UTF-8` → `FR`, iOS `en_US@rg=gbzzzz` → `US`),
|
|
1852
|
+
* uppercased. Returns `undefined` when the tag carries no two-letter region
|
|
1853
|
+
* (`en`, `es-419`, `sr-Latn`).
|
|
1854
|
+
*/
|
|
1855
|
+
function regionFromLocale(tag) {
|
|
1856
|
+
if (typeof tag !== "string" || tag.length === 0) return void 0;
|
|
1857
|
+
const parts = tag.split(/[.@]/)[0].split(/[-_]/);
|
|
1858
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1859
|
+
const part = parts[i];
|
|
1860
|
+
if (part.length === 1) break;
|
|
1861
|
+
if (/^[A-Za-z]{2}$/.test(part)) return part.toUpperCase();
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const DEVICE_OS_VALUES = [
|
|
1865
|
+
"ios",
|
|
1866
|
+
"android",
|
|
1867
|
+
"web",
|
|
1868
|
+
"macos",
|
|
1869
|
+
"windows",
|
|
1870
|
+
"linux",
|
|
1871
|
+
"other"
|
|
1872
|
+
];
|
|
1873
|
+
/**
|
|
1874
|
+
* Map a platform OS name (React Native's `Platform.OS`, for example) to an
|
|
1875
|
+
* `X-Device-OS` value. Listed names pass through lowercased; any other
|
|
1876
|
+
* non-empty name is a known-but-unlisted OS and maps to `"other"`. Empty or
|
|
1877
|
+
* missing returns `undefined`.
|
|
1878
|
+
*/
|
|
1879
|
+
function deviceOsFromPlatform(os) {
|
|
1880
|
+
if (typeof os !== "string") return void 0;
|
|
1881
|
+
const v = os.trim().toLowerCase();
|
|
1882
|
+
if (v.length === 0) return void 0;
|
|
1883
|
+
return DEVICE_OS_VALUES.includes(v) ? v : "other";
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1766
1886
|
//#endregion
|
|
1767
1887
|
//#region src/realtime.ts
|
|
1768
1888
|
const INITIAL_BACKOFF_MS = 500;
|
|
@@ -2086,7 +2206,7 @@ function delay(ms) {
|
|
|
2086
2206
|
//#endregion
|
|
2087
2207
|
//#region src/index.ts
|
|
2088
2208
|
/** SDK semver — burned in at build time. Bump alongside package.json. */
|
|
2089
|
-
const SDK_VERSION = "4.0
|
|
2209
|
+
const SDK_VERSION = "4.1.0";
|
|
2090
2210
|
/**
|
|
2091
2211
|
* Default host that serves deployed functions. `Amba.functions.invoke(name)`
|
|
2092
2212
|
* targets `https://<projectId>.<DEFAULT_FUNCTIONS_HOST>/<name>` unless a
|
|
@@ -2123,6 +2243,13 @@ function realtimeAuthHeaders() {
|
|
|
2123
2243
|
};
|
|
2124
2244
|
}
|
|
2125
2245
|
const authSubscribers = /* @__PURE__ */ new Set();
|
|
2246
|
+
/**
|
|
2247
|
+
* `Amba.onExperimentAssigned` listeners. `experiments.getVariant` reports
|
|
2248
|
+
* each result here; the reporter calls the listeners once per experiment per
|
|
2249
|
+
* user session. A sign-out (any auth change to no session) and `configure`
|
|
2250
|
+
* start a new session.
|
|
2251
|
+
*/
|
|
2252
|
+
const experimentReporter = new ExperimentAssignmentReporter();
|
|
2126
2253
|
function snapshotSession(user) {
|
|
2127
2254
|
if (!user) return null;
|
|
2128
2255
|
return {
|
|
@@ -2133,6 +2260,7 @@ function snapshotSession(user) {
|
|
|
2133
2260
|
};
|
|
2134
2261
|
}
|
|
2135
2262
|
function notifyAuthSubscribers(session) {
|
|
2263
|
+
if (session === null) experimentReporter.reset();
|
|
2136
2264
|
for (const cb of authSubscribers) try {
|
|
2137
2265
|
cb(session);
|
|
2138
2266
|
} catch (err) {
|
|
@@ -2549,6 +2677,7 @@ const P = {
|
|
|
2549
2677
|
aiAnthropic: "/v1/client/ai/anthropic/messages",
|
|
2550
2678
|
config: "/v1/client/config",
|
|
2551
2679
|
flags: "/v1/client/flags",
|
|
2680
|
+
experiments: "/v1/client/experiments",
|
|
2552
2681
|
diagnosticsPing: "/v1/client/diagnostics/ping",
|
|
2553
2682
|
achievements: "/v1/client/achievements",
|
|
2554
2683
|
challenges: "/v1/client/challenges",
|
|
@@ -2692,6 +2821,39 @@ async function deleteDirect(s, name, id) {
|
|
|
2692
2821
|
return await s.http.delete(`${P.collections}/${encodePathSegment(name)}/${encodePathSegment(id)}`) ?? { data: { deleted: true } };
|
|
2693
2822
|
}
|
|
2694
2823
|
/**
|
|
2824
|
+
* The device OS for `X-Device-OS` from React Native's `Platform.OS`
|
|
2825
|
+
* (`ios`, `android`, `web` for react-native-web, `macos`, `windows`).
|
|
2826
|
+
* `undefined` outside React Native (Node, tests).
|
|
2827
|
+
*/
|
|
2828
|
+
function detectDeviceOs() {
|
|
2829
|
+
try {
|
|
2830
|
+
const os = require("react-native").Platform?.OS;
|
|
2831
|
+
return deviceOsFromPlatform(typeof os === "string" ? os : void 0);
|
|
2832
|
+
} catch {
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* The device locale's region for `X-Device-Region`: the iOS region setting
|
|
2838
|
+
* (`AppleLocale`) or Android's locale, falling back to the JS runtime's
|
|
2839
|
+
* default locale. `undefined` when none carries a two-letter region.
|
|
2840
|
+
*/
|
|
2841
|
+
function detectDeviceRegion() {
|
|
2842
|
+
try {
|
|
2843
|
+
const rn = require("react-native");
|
|
2844
|
+
const ios = (rn.NativeModules?.["SettingsManager"])?.settings;
|
|
2845
|
+
const android = rn.NativeModules?.["I18nManager"];
|
|
2846
|
+
const tag = ios?.AppleLocale ?? android?.localeIdentifier;
|
|
2847
|
+
const region = regionFromLocale(typeof tag === "string" ? tag : void 0);
|
|
2848
|
+
if (region) return region;
|
|
2849
|
+
} catch {}
|
|
2850
|
+
try {
|
|
2851
|
+
return regionFromLocale(Intl.DateTimeFormat().resolvedOptions().locale);
|
|
2852
|
+
} catch {
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2695
2857
|
* Top-level SDK namespace for React Native.
|
|
2696
2858
|
*/
|
|
2697
2859
|
const Amba = {
|
|
@@ -2723,6 +2885,9 @@ const Amba = {
|
|
|
2723
2885
|
baseUrl,
|
|
2724
2886
|
sdkPlatform: "react-native",
|
|
2725
2887
|
sdkVersionHeader,
|
|
2888
|
+
appVersion: normalizeAppVersion(config.appVersion),
|
|
2889
|
+
deviceRegion: detectDeviceRegion(),
|
|
2890
|
+
deviceOs: detectDeviceOs(),
|
|
2726
2891
|
identity,
|
|
2727
2892
|
ensureSession: config.autoAnonymousSession === false ? void 0 : async (path) => {
|
|
2728
2893
|
if (pathSkipsLazySession(path)) return;
|
|
@@ -2735,6 +2900,7 @@ const Amba = {
|
|
|
2735
2900
|
if (prior?.offlineQueue) await prior.offlineQueue.dispose();
|
|
2736
2901
|
prior?.detachEventAppState?.();
|
|
2737
2902
|
if (prior?.eventQueue) await prior.eventQueue.dispose();
|
|
2903
|
+
experimentReporter.reset();
|
|
2738
2904
|
const nextState = {
|
|
2739
2905
|
http,
|
|
2740
2906
|
identity,
|
|
@@ -3426,6 +3592,22 @@ const Amba = {
|
|
|
3426
3592
|
}
|
|
3427
3593
|
}
|
|
3428
3594
|
},
|
|
3595
|
+
experiments: {
|
|
3596
|
+
getVariant: async (key) => {
|
|
3597
|
+
const s = getState();
|
|
3598
|
+
const userBefore = s.identity.getAppUserId();
|
|
3599
|
+
const env = await s.http.get(`${P.experiments}/${encodePathSegment(key)}/assignment`);
|
|
3600
|
+
return experimentReporter.report({
|
|
3601
|
+
experimentKey: env.data.experiment_key,
|
|
3602
|
+
variant: env.data.variant,
|
|
3603
|
+
bucketingId: userBefore ?? s.identity.getAppUserId() ?? ""
|
|
3604
|
+
});
|
|
3605
|
+
},
|
|
3606
|
+
trackExposure: async (key) => {
|
|
3607
|
+
await getState().http.post(`${P.experiments}/${encodePathSegment(key)}/exposure`, {});
|
|
3608
|
+
}
|
|
3609
|
+
},
|
|
3610
|
+
onExperimentAssigned: (listener) => experimentReporter.subscribe(listener),
|
|
3429
3611
|
diagnostics: { ping: async () => {
|
|
3430
3612
|
return await getState().http.post(P.diagnosticsPing, {});
|
|
3431
3613
|
} },
|