@sendoracloud/sdk-react-native 1.5.0 → 1.6.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/dist/index.cjs +92 -22
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +92 -22
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1760,15 +1760,16 @@ var import_react_native3 = require("react-native");
|
|
|
1760
1760
|
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
1761
1761
|
function resolveFlags(cfg) {
|
|
1762
1762
|
if (cfg === false) {
|
|
1763
|
-
return { appOpen: false, sessionStart: false, appBackground: false };
|
|
1763
|
+
return { appOpen: false, sessionStart: false, appBackground: false, engagement: false };
|
|
1764
1764
|
}
|
|
1765
1765
|
if (cfg === true || cfg === void 0) {
|
|
1766
|
-
return { appOpen: true, sessionStart: true, appBackground: true };
|
|
1766
|
+
return { appOpen: true, sessionStart: true, appBackground: true, engagement: true };
|
|
1767
1767
|
}
|
|
1768
1768
|
return {
|
|
1769
1769
|
appOpen: cfg.appOpen ?? true,
|
|
1770
1770
|
sessionStart: cfg.sessionStart ?? true,
|
|
1771
|
-
appBackground: cfg.appBackground ?? true
|
|
1771
|
+
appBackground: cfg.appBackground ?? true,
|
|
1772
|
+
engagement: cfg.engagement ?? true
|
|
1772
1773
|
};
|
|
1773
1774
|
}
|
|
1774
1775
|
function installAutoTrack(args) {
|
|
@@ -1799,15 +1800,21 @@ function installAutoTrack(args) {
|
|
|
1799
1800
|
if (flags.appOpen) {
|
|
1800
1801
|
args.fire("app.opened", { sessionId });
|
|
1801
1802
|
}
|
|
1802
|
-
if (flags.appBackground) {
|
|
1803
|
+
if (flags.appBackground || flags.engagement) {
|
|
1803
1804
|
const AppState = import_react_native3.AppState;
|
|
1804
1805
|
if (AppState && typeof AppState.addEventListener === "function") {
|
|
1805
1806
|
const handler = (state) => {
|
|
1806
1807
|
ensureSession(true);
|
|
1807
1808
|
if (state === "background" || state === "inactive") {
|
|
1808
|
-
|
|
1809
|
+
if (flags.engagement) args.onForeground?.(false);
|
|
1810
|
+
if (flags.appBackground) {
|
|
1811
|
+
args.fire("app.backgrounded", { sessionId: args.getSessionId() });
|
|
1812
|
+
}
|
|
1809
1813
|
} else if (state === "active") {
|
|
1810
|
-
|
|
1814
|
+
if (flags.appBackground) {
|
|
1815
|
+
args.fire("app.foregrounded", { sessionId: args.getSessionId() });
|
|
1816
|
+
}
|
|
1817
|
+
if (flags.engagement) args.onForeground?.(true);
|
|
1811
1818
|
}
|
|
1812
1819
|
};
|
|
1813
1820
|
const sub = AppState.addEventListener("change", handler);
|
|
@@ -1848,6 +1855,8 @@ var MAX_PUSH_METADATA_BYTES = 4096;
|
|
|
1848
1855
|
var MAX_USER_ID_LEN = 256;
|
|
1849
1856
|
var MAX_TRAITS_BYTES = 32 * 1024;
|
|
1850
1857
|
var MAX_PROPERTIES_BYTES = 32 * 1024;
|
|
1858
|
+
var MIN_ENGAGEMENT_MS = 250;
|
|
1859
|
+
var MAX_ENGAGEMENT_MS = 6 * 60 * 60 * 1e3;
|
|
1851
1860
|
function assertCSPRNG() {
|
|
1852
1861
|
const g = globalThis;
|
|
1853
1862
|
if (g.crypto?.getRandomValues) {
|
|
@@ -1898,6 +1907,12 @@ var SendoraSDK = class {
|
|
|
1898
1907
|
this.debug = false;
|
|
1899
1908
|
this.sessionId = "";
|
|
1900
1909
|
this.autoTrackCleanup = null;
|
|
1910
|
+
// --- Engagement time (foreground-only, Wave 75) ---
|
|
1911
|
+
this.engEnabled = false;
|
|
1912
|
+
this.engScreen = null;
|
|
1913
|
+
this.engAccumMs = 0;
|
|
1914
|
+
// >0 = accumulating since this timestamp; 0 = paused (backgrounded).
|
|
1915
|
+
this.engSegmentStart = 0;
|
|
1901
1916
|
/**
|
|
1902
1917
|
* Lazy-allocated stable session id for chatbot conversations.
|
|
1903
1918
|
* Survives only the JS lifetime — chatbot multi-turn context lives
|
|
@@ -1988,12 +2003,17 @@ var SendoraSDK = class {
|
|
|
1988
2003
|
});
|
|
1989
2004
|
this.ready = true;
|
|
1990
2005
|
if (config.autoTrack !== false) {
|
|
2006
|
+
this.engEnabled = config.autoTrack === true || config.autoTrack === void 0 || config.autoTrack.engagement !== false;
|
|
1991
2007
|
this.autoTrackCleanup = installAutoTrack({
|
|
1992
2008
|
config: this.config,
|
|
1993
2009
|
fire: (eventType, properties) => this.track(eventType, properties),
|
|
1994
2010
|
getSessionId: () => this.sessionId,
|
|
1995
2011
|
setSessionId: (id) => {
|
|
1996
2012
|
this.sessionId = id;
|
|
2013
|
+
},
|
|
2014
|
+
onForeground: (active) => {
|
|
2015
|
+
if (active) this.engResume();
|
|
2016
|
+
else this.engFlush();
|
|
1997
2017
|
}
|
|
1998
2018
|
});
|
|
1999
2019
|
}
|
|
@@ -2057,10 +2077,53 @@ var SendoraSDK = class {
|
|
|
2057
2077
|
if (typeof name !== "string" || name.length === 0 || name.length > 256) {
|
|
2058
2078
|
throw new Error("[sendoracloud] screen name must be a 1-256 char string.");
|
|
2059
2079
|
}
|
|
2080
|
+
this.engFlush();
|
|
2060
2081
|
void this.sendEvent("screen", {
|
|
2061
2082
|
eventType: "screen.viewed",
|
|
2062
2083
|
properties: { screenName: name, ...properties ?? {} }
|
|
2063
2084
|
});
|
|
2085
|
+
this.engEnter(name);
|
|
2086
|
+
}
|
|
2087
|
+
// --- Engagement timer (foreground-only, Wave 75) ---
|
|
2088
|
+
/** Bank the in-flight foreground segment into the accumulator + pause. */
|
|
2089
|
+
engBank() {
|
|
2090
|
+
if (this.engSegmentStart > 0) {
|
|
2091
|
+
this.engAccumMs += Date.now() - this.engSegmentStart;
|
|
2092
|
+
this.engSegmentStart = 0;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Emit `app.engagement` for the current screen with foreground-only
|
|
2097
|
+
* durationMs, then reset the accumulator. Sub-threshold (redirect
|
|
2098
|
+
* flicker) spans are dropped; absurd spans clamp to a 6h ceiling.
|
|
2099
|
+
* Never throws — engagement is best-effort instrumentation.
|
|
2100
|
+
*/
|
|
2101
|
+
engFlush() {
|
|
2102
|
+
if (!this.engEnabled || this.engScreen === null) return;
|
|
2103
|
+
this.engBank();
|
|
2104
|
+
let ms = this.engAccumMs;
|
|
2105
|
+
this.engAccumMs = 0;
|
|
2106
|
+
if (ms < MIN_ENGAGEMENT_MS) return;
|
|
2107
|
+
if (ms > MAX_ENGAGEMENT_MS) ms = MAX_ENGAGEMENT_MS;
|
|
2108
|
+
try {
|
|
2109
|
+
void this.sendEvent("track", {
|
|
2110
|
+
eventType: "app.engagement",
|
|
2111
|
+
properties: { durationMs: Math.round(ms), screen: this.engScreen, sessionId: this.sessionId }
|
|
2112
|
+
});
|
|
2113
|
+
} catch {
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
/** Start measuring a new screen (foreground from now). */
|
|
2117
|
+
engEnter(name) {
|
|
2118
|
+
if (!this.engEnabled) return;
|
|
2119
|
+
this.engScreen = name;
|
|
2120
|
+
this.engAccumMs = 0;
|
|
2121
|
+
this.engSegmentStart = Date.now();
|
|
2122
|
+
}
|
|
2123
|
+
/** Resume accumulation after the app returns to foreground. */
|
|
2124
|
+
engResume() {
|
|
2125
|
+
if (!this.engEnabled || this.engScreen === null) return;
|
|
2126
|
+
if (this.engSegmentStart === 0) this.engSegmentStart = Date.now();
|
|
2064
2127
|
}
|
|
2065
2128
|
/**
|
|
2066
2129
|
* Register a device push token (APNs or FCM) so server-side
|
|
@@ -2086,7 +2149,13 @@ var SendoraSDK = class {
|
|
|
2086
2149
|
{ apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
|
|
2087
2150
|
"/api/v1/push/tokens",
|
|
2088
2151
|
{
|
|
2089
|
-
|
|
2152
|
+
// Conditional spread — the backend's registerPushTokenSchema makes
|
|
2153
|
+
// userId `.optional()`, which accepts a missing key but REJECTS
|
|
2154
|
+
// `null` (422 "userId: Expected string, received null"). Anonymous
|
|
2155
|
+
// installs (pre-identify()) carry a null userId, so OMIT the field
|
|
2156
|
+
// rather than send null — anon devices must still be able to
|
|
2157
|
+
// register. Mirrors the same conditional-spread sendEvent() uses.
|
|
2158
|
+
...this.userId ? { userId: this.userId } : {},
|
|
2090
2159
|
anonymousId: this.anonId,
|
|
2091
2160
|
token: reg.token,
|
|
2092
2161
|
platform: reg.platform,
|
|
@@ -2128,7 +2197,7 @@ var SendoraSDK = class {
|
|
|
2128
2197
|
if (clickAction !== void 0) body.clickAction = clickAction;
|
|
2129
2198
|
await post(
|
|
2130
2199
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2131
|
-
"/push/track-open",
|
|
2200
|
+
"/api/v1/push/track-open",
|
|
2132
2201
|
body
|
|
2133
2202
|
);
|
|
2134
2203
|
},
|
|
@@ -2147,7 +2216,7 @@ var SendoraSDK = class {
|
|
|
2147
2216
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.liveActivities.startToken().");
|
|
2148
2217
|
const res = await post(
|
|
2149
2218
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2150
|
-
"/push/live-activities/start-token",
|
|
2219
|
+
"/api/v1/push/live-activities/start-token",
|
|
2151
2220
|
{
|
|
2152
2221
|
pushToken: input.pushToken,
|
|
2153
2222
|
activityType: input.activityType,
|
|
@@ -2181,7 +2250,7 @@ var SendoraSDK = class {
|
|
|
2181
2250
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.geofences.list().");
|
|
2182
2251
|
const res = await getJson(
|
|
2183
2252
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2184
|
-
"/push/geofences/list-for-device"
|
|
2253
|
+
"/api/v1/push/geofences/list-for-device"
|
|
2185
2254
|
);
|
|
2186
2255
|
if (!res || !res.ok) {
|
|
2187
2256
|
throw new Error(`[sendoracloud] geofences list failed (HTTP ${res?.status ?? "network"}).`);
|
|
@@ -2199,7 +2268,7 @@ var SendoraSDK = class {
|
|
|
2199
2268
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.geofences.recordEvent().");
|
|
2200
2269
|
const res = await post(
|
|
2201
2270
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2202
|
-
"/push/geofences/event",
|
|
2271
|
+
"/api/v1/push/geofences/event",
|
|
2203
2272
|
{
|
|
2204
2273
|
geofenceId: input.geofenceId,
|
|
2205
2274
|
kind: input.kind,
|
|
@@ -2243,7 +2312,7 @@ var SendoraSDK = class {
|
|
|
2243
2312
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before attribution.reportInstall().");
|
|
2244
2313
|
const res = await post(
|
|
2245
2314
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2246
|
-
"/attribution/install",
|
|
2315
|
+
"/api/v1/attribution/install",
|
|
2247
2316
|
input ?? {}
|
|
2248
2317
|
);
|
|
2249
2318
|
if (!res || !res.ok) throw new Error(`[sendoracloud] attribution.reportInstall failed (${res?.status ?? "network"})`);
|
|
@@ -2255,7 +2324,7 @@ var SendoraSDK = class {
|
|
|
2255
2324
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before attribution.checkDeferred().");
|
|
2256
2325
|
const res = await post(
|
|
2257
2326
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2258
|
-
"/attribution/deferred",
|
|
2327
|
+
"/api/v1/attribution/deferred",
|
|
2259
2328
|
input ?? {}
|
|
2260
2329
|
);
|
|
2261
2330
|
if (!res || !res.ok) throw new Error(`[sendoracloud] attribution.checkDeferred failed (${res?.status ?? "network"})`);
|
|
@@ -2289,7 +2358,7 @@ var SendoraSDK = class {
|
|
|
2289
2358
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before support.createTicket().");
|
|
2290
2359
|
const res = await post(
|
|
2291
2360
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2292
|
-
"/support/tickets",
|
|
2361
|
+
"/api/v1/support/tickets",
|
|
2293
2362
|
input
|
|
2294
2363
|
);
|
|
2295
2364
|
if (!res || !res.ok) throw new Error(`[sendoracloud] support.createTicket failed (${res?.status ?? "network"})`);
|
|
@@ -2301,7 +2370,7 @@ var SendoraSDK = class {
|
|
|
2301
2370
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before support.submitCsat().");
|
|
2302
2371
|
const res = await post(
|
|
2303
2372
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2304
|
-
"/support/csat",
|
|
2373
|
+
"/api/v1/support/csat",
|
|
2305
2374
|
input
|
|
2306
2375
|
);
|
|
2307
2376
|
if (!res || !res.ok) throw new Error(`[sendoracloud] support.submitCsat failed (${res?.status ?? "network"})`);
|
|
@@ -2390,7 +2459,7 @@ var SendoraSDK = class {
|
|
|
2390
2459
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before consent.record().");
|
|
2391
2460
|
const res = await post(
|
|
2392
2461
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2393
|
-
"/consent/record",
|
|
2462
|
+
"/api/v1/consent/record",
|
|
2394
2463
|
input
|
|
2395
2464
|
);
|
|
2396
2465
|
if (!res || !res.ok) throw new Error(`[sendoracloud] consent.record failed (${res?.status ?? "network"})`);
|
|
@@ -2402,7 +2471,7 @@ var SendoraSDK = class {
|
|
|
2402
2471
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before consent.requestDeletion().");
|
|
2403
2472
|
const res = await post(
|
|
2404
2473
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2405
|
-
"/consent/deletion-request",
|
|
2474
|
+
"/api/v1/consent/deletion-request",
|
|
2406
2475
|
input
|
|
2407
2476
|
);
|
|
2408
2477
|
if (!res || !res.ok) throw new Error(`[sendoracloud] consent.requestDeletion failed (${res?.status ?? "network"})`);
|
|
@@ -2453,7 +2522,7 @@ var SendoraSDK = class {
|
|
|
2453
2522
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before surveys.submitResponse().");
|
|
2454
2523
|
const res = await post(
|
|
2455
2524
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2456
|
-
"/surveys/responses",
|
|
2525
|
+
"/api/v1/surveys/responses",
|
|
2457
2526
|
{ completed: true, ...input }
|
|
2458
2527
|
);
|
|
2459
2528
|
if (!res || !res.ok) throw new Error(`[sendoracloud] surveys.submitResponse failed (${res?.status ?? "network"})`);
|
|
@@ -2503,7 +2572,7 @@ var SendoraSDK = class {
|
|
|
2503
2572
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before messages.recordImpression().");
|
|
2504
2573
|
const res = await post(
|
|
2505
2574
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2506
|
-
"/in-app-messages/impressions",
|
|
2575
|
+
"/api/v1/in-app-messages/impressions",
|
|
2507
2576
|
input
|
|
2508
2577
|
);
|
|
2509
2578
|
if (!res || !res.ok) throw new Error(`[sendoracloud] messages.recordImpression failed (${res?.status ?? "network"})`);
|
|
@@ -2541,7 +2610,7 @@ var SendoraSDK = class {
|
|
|
2541
2610
|
};
|
|
2542
2611
|
const res = await post(
|
|
2543
2612
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2544
|
-
"/feature-flags/evaluate",
|
|
2613
|
+
"/api/v1/feature-flags/evaluate",
|
|
2545
2614
|
body
|
|
2546
2615
|
);
|
|
2547
2616
|
if (!res || !res.ok) throw new Error(`[sendoracloud] flags.evaluate failed (${res?.status ?? "network"})`);
|
|
@@ -2558,7 +2627,7 @@ var SendoraSDK = class {
|
|
|
2558
2627
|
};
|
|
2559
2628
|
const res = await post(
|
|
2560
2629
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2561
|
-
"/feature-flags/evaluate-all",
|
|
2630
|
+
"/api/v1/feature-flags/evaluate-all",
|
|
2562
2631
|
body
|
|
2563
2632
|
);
|
|
2564
2633
|
if (!res || !res.ok) throw new Error(`[sendoracloud] flags.evaluateAll failed (${res?.status ?? "network"})`);
|
|
@@ -2593,7 +2662,7 @@ var SendoraSDK = class {
|
|
|
2593
2662
|
};
|
|
2594
2663
|
const res = await post(
|
|
2595
2664
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2596
|
-
"/chatbot/message",
|
|
2665
|
+
"/api/v1/chatbot/message",
|
|
2597
2666
|
body
|
|
2598
2667
|
);
|
|
2599
2668
|
if (!res || !res.ok) throw new Error(`[sendoracloud] chatbot.sendMessage failed (${res?.status ?? "network"})`);
|
|
@@ -2621,6 +2690,7 @@ var SendoraSDK = class {
|
|
|
2621
2690
|
}
|
|
2622
2691
|
/** Tear down auto-track listeners. Call in test harnesses. */
|
|
2623
2692
|
destroy() {
|
|
2693
|
+
this.engFlush();
|
|
2624
2694
|
if (this.autoTrackCleanup) {
|
|
2625
2695
|
this.autoTrackCleanup();
|
|
2626
2696
|
this.autoTrackCleanup = null;
|
package/dist/index.d.cts
CHANGED
|
@@ -728,6 +728,10 @@ interface SendoraConfig {
|
|
|
728
728
|
* (idle >`sessionIdleMs` closes session). Default true.
|
|
729
729
|
* - `appBackground` — emit `app.backgrounded`/`app.foregrounded` on
|
|
730
730
|
* AppState transitions. Default true.
|
|
731
|
+
* - `engagement` — emit `app.engagement` with foreground-only `durationMs`
|
|
732
|
+
* per screen on the next `screen()` call + on app-background (GA4
|
|
733
|
+
* user_engagement parity). Default true. Only measures named screens —
|
|
734
|
+
* it has no effect until you start calling `screen()`.
|
|
731
735
|
*
|
|
732
736
|
* Note: screen view tracking is NOT auto — RN has no canonical router.
|
|
733
737
|
* Call `SendoraCloud.screen(name)` from your navigation listener.
|
|
@@ -736,6 +740,7 @@ interface SendoraConfig {
|
|
|
736
740
|
appOpen?: boolean;
|
|
737
741
|
sessionStart?: boolean;
|
|
738
742
|
appBackground?: boolean;
|
|
743
|
+
engagement?: boolean;
|
|
739
744
|
};
|
|
740
745
|
/** Idle threshold (ms) for session expiry; default 30 minutes. */
|
|
741
746
|
sessionIdleMs?: number;
|
|
@@ -827,6 +832,10 @@ declare class SendoraSDK {
|
|
|
827
832
|
private debug;
|
|
828
833
|
private sessionId;
|
|
829
834
|
private autoTrackCleanup;
|
|
835
|
+
private engEnabled;
|
|
836
|
+
private engScreen;
|
|
837
|
+
private engAccumMs;
|
|
838
|
+
private engSegmentStart;
|
|
830
839
|
/** Lazy-initialised in init() once we know the apiUrl + publicKey. */
|
|
831
840
|
auth: Auth;
|
|
832
841
|
/** Lazy-initialised in init(). Deep-link surface: create + handleUniversalLink + matchDeferred. */
|
|
@@ -847,6 +856,19 @@ declare class SendoraSDK {
|
|
|
847
856
|
track(eventType: string, properties?: TrackProperties): void;
|
|
848
857
|
/** RN equivalent of page view — call on route / screen change. */
|
|
849
858
|
screen(name: string, properties?: TrackProperties): void;
|
|
859
|
+
/** Bank the in-flight foreground segment into the accumulator + pause. */
|
|
860
|
+
private engBank;
|
|
861
|
+
/**
|
|
862
|
+
* Emit `app.engagement` for the current screen with foreground-only
|
|
863
|
+
* durationMs, then reset the accumulator. Sub-threshold (redirect
|
|
864
|
+
* flicker) spans are dropped; absurd spans clamp to a 6h ceiling.
|
|
865
|
+
* Never throws — engagement is best-effort instrumentation.
|
|
866
|
+
*/
|
|
867
|
+
private engFlush;
|
|
868
|
+
/** Start measuring a new screen (foreground from now). */
|
|
869
|
+
private engEnter;
|
|
870
|
+
/** Resume accumulation after the app returns to foreground. */
|
|
871
|
+
private engResume;
|
|
850
872
|
/**
|
|
851
873
|
* Register a device push token (APNs or FCM) so server-side
|
|
852
874
|
* workflows can target this user. The caller is responsible for
|
package/dist/index.d.ts
CHANGED
|
@@ -728,6 +728,10 @@ interface SendoraConfig {
|
|
|
728
728
|
* (idle >`sessionIdleMs` closes session). Default true.
|
|
729
729
|
* - `appBackground` — emit `app.backgrounded`/`app.foregrounded` on
|
|
730
730
|
* AppState transitions. Default true.
|
|
731
|
+
* - `engagement` — emit `app.engagement` with foreground-only `durationMs`
|
|
732
|
+
* per screen on the next `screen()` call + on app-background (GA4
|
|
733
|
+
* user_engagement parity). Default true. Only measures named screens —
|
|
734
|
+
* it has no effect until you start calling `screen()`.
|
|
731
735
|
*
|
|
732
736
|
* Note: screen view tracking is NOT auto — RN has no canonical router.
|
|
733
737
|
* Call `SendoraCloud.screen(name)` from your navigation listener.
|
|
@@ -736,6 +740,7 @@ interface SendoraConfig {
|
|
|
736
740
|
appOpen?: boolean;
|
|
737
741
|
sessionStart?: boolean;
|
|
738
742
|
appBackground?: boolean;
|
|
743
|
+
engagement?: boolean;
|
|
739
744
|
};
|
|
740
745
|
/** Idle threshold (ms) for session expiry; default 30 minutes. */
|
|
741
746
|
sessionIdleMs?: number;
|
|
@@ -827,6 +832,10 @@ declare class SendoraSDK {
|
|
|
827
832
|
private debug;
|
|
828
833
|
private sessionId;
|
|
829
834
|
private autoTrackCleanup;
|
|
835
|
+
private engEnabled;
|
|
836
|
+
private engScreen;
|
|
837
|
+
private engAccumMs;
|
|
838
|
+
private engSegmentStart;
|
|
830
839
|
/** Lazy-initialised in init() once we know the apiUrl + publicKey. */
|
|
831
840
|
auth: Auth;
|
|
832
841
|
/** Lazy-initialised in init(). Deep-link surface: create + handleUniversalLink + matchDeferred. */
|
|
@@ -847,6 +856,19 @@ declare class SendoraSDK {
|
|
|
847
856
|
track(eventType: string, properties?: TrackProperties): void;
|
|
848
857
|
/** RN equivalent of page view — call on route / screen change. */
|
|
849
858
|
screen(name: string, properties?: TrackProperties): void;
|
|
859
|
+
/** Bank the in-flight foreground segment into the accumulator + pause. */
|
|
860
|
+
private engBank;
|
|
861
|
+
/**
|
|
862
|
+
* Emit `app.engagement` for the current screen with foreground-only
|
|
863
|
+
* durationMs, then reset the accumulator. Sub-threshold (redirect
|
|
864
|
+
* flicker) spans are dropped; absurd spans clamp to a 6h ceiling.
|
|
865
|
+
* Never throws — engagement is best-effort instrumentation.
|
|
866
|
+
*/
|
|
867
|
+
private engFlush;
|
|
868
|
+
/** Start measuring a new screen (foreground from now). */
|
|
869
|
+
private engEnter;
|
|
870
|
+
/** Resume accumulation after the app returns to foreground. */
|
|
871
|
+
private engResume;
|
|
850
872
|
/**
|
|
851
873
|
* Register a device push token (APNs or FCM) so server-side
|
|
852
874
|
* workflows can target this user. The caller is responsible for
|
package/dist/index.js
CHANGED
|
@@ -1722,15 +1722,16 @@ import { AppState as RnAppState2 } from "react-native";
|
|
|
1722
1722
|
var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
|
|
1723
1723
|
function resolveFlags(cfg) {
|
|
1724
1724
|
if (cfg === false) {
|
|
1725
|
-
return { appOpen: false, sessionStart: false, appBackground: false };
|
|
1725
|
+
return { appOpen: false, sessionStart: false, appBackground: false, engagement: false };
|
|
1726
1726
|
}
|
|
1727
1727
|
if (cfg === true || cfg === void 0) {
|
|
1728
|
-
return { appOpen: true, sessionStart: true, appBackground: true };
|
|
1728
|
+
return { appOpen: true, sessionStart: true, appBackground: true, engagement: true };
|
|
1729
1729
|
}
|
|
1730
1730
|
return {
|
|
1731
1731
|
appOpen: cfg.appOpen ?? true,
|
|
1732
1732
|
sessionStart: cfg.sessionStart ?? true,
|
|
1733
|
-
appBackground: cfg.appBackground ?? true
|
|
1733
|
+
appBackground: cfg.appBackground ?? true,
|
|
1734
|
+
engagement: cfg.engagement ?? true
|
|
1734
1735
|
};
|
|
1735
1736
|
}
|
|
1736
1737
|
function installAutoTrack(args) {
|
|
@@ -1761,15 +1762,21 @@ function installAutoTrack(args) {
|
|
|
1761
1762
|
if (flags.appOpen) {
|
|
1762
1763
|
args.fire("app.opened", { sessionId });
|
|
1763
1764
|
}
|
|
1764
|
-
if (flags.appBackground) {
|
|
1765
|
+
if (flags.appBackground || flags.engagement) {
|
|
1765
1766
|
const AppState = RnAppState2;
|
|
1766
1767
|
if (AppState && typeof AppState.addEventListener === "function") {
|
|
1767
1768
|
const handler = (state) => {
|
|
1768
1769
|
ensureSession(true);
|
|
1769
1770
|
if (state === "background" || state === "inactive") {
|
|
1770
|
-
|
|
1771
|
+
if (flags.engagement) args.onForeground?.(false);
|
|
1772
|
+
if (flags.appBackground) {
|
|
1773
|
+
args.fire("app.backgrounded", { sessionId: args.getSessionId() });
|
|
1774
|
+
}
|
|
1771
1775
|
} else if (state === "active") {
|
|
1772
|
-
|
|
1776
|
+
if (flags.appBackground) {
|
|
1777
|
+
args.fire("app.foregrounded", { sessionId: args.getSessionId() });
|
|
1778
|
+
}
|
|
1779
|
+
if (flags.engagement) args.onForeground?.(true);
|
|
1773
1780
|
}
|
|
1774
1781
|
};
|
|
1775
1782
|
const sub = AppState.addEventListener("change", handler);
|
|
@@ -1810,6 +1817,8 @@ var MAX_PUSH_METADATA_BYTES = 4096;
|
|
|
1810
1817
|
var MAX_USER_ID_LEN = 256;
|
|
1811
1818
|
var MAX_TRAITS_BYTES = 32 * 1024;
|
|
1812
1819
|
var MAX_PROPERTIES_BYTES = 32 * 1024;
|
|
1820
|
+
var MIN_ENGAGEMENT_MS = 250;
|
|
1821
|
+
var MAX_ENGAGEMENT_MS = 6 * 60 * 60 * 1e3;
|
|
1813
1822
|
function assertCSPRNG() {
|
|
1814
1823
|
const g = globalThis;
|
|
1815
1824
|
if (g.crypto?.getRandomValues) {
|
|
@@ -1860,6 +1869,12 @@ var SendoraSDK = class {
|
|
|
1860
1869
|
this.debug = false;
|
|
1861
1870
|
this.sessionId = "";
|
|
1862
1871
|
this.autoTrackCleanup = null;
|
|
1872
|
+
// --- Engagement time (foreground-only, Wave 75) ---
|
|
1873
|
+
this.engEnabled = false;
|
|
1874
|
+
this.engScreen = null;
|
|
1875
|
+
this.engAccumMs = 0;
|
|
1876
|
+
// >0 = accumulating since this timestamp; 0 = paused (backgrounded).
|
|
1877
|
+
this.engSegmentStart = 0;
|
|
1863
1878
|
/**
|
|
1864
1879
|
* Lazy-allocated stable session id for chatbot conversations.
|
|
1865
1880
|
* Survives only the JS lifetime — chatbot multi-turn context lives
|
|
@@ -1950,12 +1965,17 @@ var SendoraSDK = class {
|
|
|
1950
1965
|
});
|
|
1951
1966
|
this.ready = true;
|
|
1952
1967
|
if (config.autoTrack !== false) {
|
|
1968
|
+
this.engEnabled = config.autoTrack === true || config.autoTrack === void 0 || config.autoTrack.engagement !== false;
|
|
1953
1969
|
this.autoTrackCleanup = installAutoTrack({
|
|
1954
1970
|
config: this.config,
|
|
1955
1971
|
fire: (eventType, properties) => this.track(eventType, properties),
|
|
1956
1972
|
getSessionId: () => this.sessionId,
|
|
1957
1973
|
setSessionId: (id) => {
|
|
1958
1974
|
this.sessionId = id;
|
|
1975
|
+
},
|
|
1976
|
+
onForeground: (active) => {
|
|
1977
|
+
if (active) this.engResume();
|
|
1978
|
+
else this.engFlush();
|
|
1959
1979
|
}
|
|
1960
1980
|
});
|
|
1961
1981
|
}
|
|
@@ -2019,10 +2039,53 @@ var SendoraSDK = class {
|
|
|
2019
2039
|
if (typeof name !== "string" || name.length === 0 || name.length > 256) {
|
|
2020
2040
|
throw new Error("[sendoracloud] screen name must be a 1-256 char string.");
|
|
2021
2041
|
}
|
|
2042
|
+
this.engFlush();
|
|
2022
2043
|
void this.sendEvent("screen", {
|
|
2023
2044
|
eventType: "screen.viewed",
|
|
2024
2045
|
properties: { screenName: name, ...properties ?? {} }
|
|
2025
2046
|
});
|
|
2047
|
+
this.engEnter(name);
|
|
2048
|
+
}
|
|
2049
|
+
// --- Engagement timer (foreground-only, Wave 75) ---
|
|
2050
|
+
/** Bank the in-flight foreground segment into the accumulator + pause. */
|
|
2051
|
+
engBank() {
|
|
2052
|
+
if (this.engSegmentStart > 0) {
|
|
2053
|
+
this.engAccumMs += Date.now() - this.engSegmentStart;
|
|
2054
|
+
this.engSegmentStart = 0;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Emit `app.engagement` for the current screen with foreground-only
|
|
2059
|
+
* durationMs, then reset the accumulator. Sub-threshold (redirect
|
|
2060
|
+
* flicker) spans are dropped; absurd spans clamp to a 6h ceiling.
|
|
2061
|
+
* Never throws — engagement is best-effort instrumentation.
|
|
2062
|
+
*/
|
|
2063
|
+
engFlush() {
|
|
2064
|
+
if (!this.engEnabled || this.engScreen === null) return;
|
|
2065
|
+
this.engBank();
|
|
2066
|
+
let ms = this.engAccumMs;
|
|
2067
|
+
this.engAccumMs = 0;
|
|
2068
|
+
if (ms < MIN_ENGAGEMENT_MS) return;
|
|
2069
|
+
if (ms > MAX_ENGAGEMENT_MS) ms = MAX_ENGAGEMENT_MS;
|
|
2070
|
+
try {
|
|
2071
|
+
void this.sendEvent("track", {
|
|
2072
|
+
eventType: "app.engagement",
|
|
2073
|
+
properties: { durationMs: Math.round(ms), screen: this.engScreen, sessionId: this.sessionId }
|
|
2074
|
+
});
|
|
2075
|
+
} catch {
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/** Start measuring a new screen (foreground from now). */
|
|
2079
|
+
engEnter(name) {
|
|
2080
|
+
if (!this.engEnabled) return;
|
|
2081
|
+
this.engScreen = name;
|
|
2082
|
+
this.engAccumMs = 0;
|
|
2083
|
+
this.engSegmentStart = Date.now();
|
|
2084
|
+
}
|
|
2085
|
+
/** Resume accumulation after the app returns to foreground. */
|
|
2086
|
+
engResume() {
|
|
2087
|
+
if (!this.engEnabled || this.engScreen === null) return;
|
|
2088
|
+
if (this.engSegmentStart === 0) this.engSegmentStart = Date.now();
|
|
2026
2089
|
}
|
|
2027
2090
|
/**
|
|
2028
2091
|
* Register a device push token (APNs or FCM) so server-side
|
|
@@ -2048,7 +2111,13 @@ var SendoraSDK = class {
|
|
|
2048
2111
|
{ apiUrl: this.config.apiUrl, publicKey: this.config.publicKey, debug: this.debug },
|
|
2049
2112
|
"/api/v1/push/tokens",
|
|
2050
2113
|
{
|
|
2051
|
-
|
|
2114
|
+
// Conditional spread — the backend's registerPushTokenSchema makes
|
|
2115
|
+
// userId `.optional()`, which accepts a missing key but REJECTS
|
|
2116
|
+
// `null` (422 "userId: Expected string, received null"). Anonymous
|
|
2117
|
+
// installs (pre-identify()) carry a null userId, so OMIT the field
|
|
2118
|
+
// rather than send null — anon devices must still be able to
|
|
2119
|
+
// register. Mirrors the same conditional-spread sendEvent() uses.
|
|
2120
|
+
...this.userId ? { userId: this.userId } : {},
|
|
2052
2121
|
anonymousId: this.anonId,
|
|
2053
2122
|
token: reg.token,
|
|
2054
2123
|
platform: reg.platform,
|
|
@@ -2090,7 +2159,7 @@ var SendoraSDK = class {
|
|
|
2090
2159
|
if (clickAction !== void 0) body.clickAction = clickAction;
|
|
2091
2160
|
await post(
|
|
2092
2161
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2093
|
-
"/push/track-open",
|
|
2162
|
+
"/api/v1/push/track-open",
|
|
2094
2163
|
body
|
|
2095
2164
|
);
|
|
2096
2165
|
},
|
|
@@ -2109,7 +2178,7 @@ var SendoraSDK = class {
|
|
|
2109
2178
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.liveActivities.startToken().");
|
|
2110
2179
|
const res = await post(
|
|
2111
2180
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2112
|
-
"/push/live-activities/start-token",
|
|
2181
|
+
"/api/v1/push/live-activities/start-token",
|
|
2113
2182
|
{
|
|
2114
2183
|
pushToken: input.pushToken,
|
|
2115
2184
|
activityType: input.activityType,
|
|
@@ -2143,7 +2212,7 @@ var SendoraSDK = class {
|
|
|
2143
2212
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.geofences.list().");
|
|
2144
2213
|
const res = await getJson(
|
|
2145
2214
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2146
|
-
"/push/geofences/list-for-device"
|
|
2215
|
+
"/api/v1/push/geofences/list-for-device"
|
|
2147
2216
|
);
|
|
2148
2217
|
if (!res || !res.ok) {
|
|
2149
2218
|
throw new Error(`[sendoracloud] geofences list failed (HTTP ${res?.status ?? "network"}).`);
|
|
@@ -2161,7 +2230,7 @@ var SendoraSDK = class {
|
|
|
2161
2230
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before push.geofences.recordEvent().");
|
|
2162
2231
|
const res = await post(
|
|
2163
2232
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2164
|
-
"/push/geofences/event",
|
|
2233
|
+
"/api/v1/push/geofences/event",
|
|
2165
2234
|
{
|
|
2166
2235
|
geofenceId: input.geofenceId,
|
|
2167
2236
|
kind: input.kind,
|
|
@@ -2205,7 +2274,7 @@ var SendoraSDK = class {
|
|
|
2205
2274
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before attribution.reportInstall().");
|
|
2206
2275
|
const res = await post(
|
|
2207
2276
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2208
|
-
"/attribution/install",
|
|
2277
|
+
"/api/v1/attribution/install",
|
|
2209
2278
|
input ?? {}
|
|
2210
2279
|
);
|
|
2211
2280
|
if (!res || !res.ok) throw new Error(`[sendoracloud] attribution.reportInstall failed (${res?.status ?? "network"})`);
|
|
@@ -2217,7 +2286,7 @@ var SendoraSDK = class {
|
|
|
2217
2286
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before attribution.checkDeferred().");
|
|
2218
2287
|
const res = await post(
|
|
2219
2288
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2220
|
-
"/attribution/deferred",
|
|
2289
|
+
"/api/v1/attribution/deferred",
|
|
2221
2290
|
input ?? {}
|
|
2222
2291
|
);
|
|
2223
2292
|
if (!res || !res.ok) throw new Error(`[sendoracloud] attribution.checkDeferred failed (${res?.status ?? "network"})`);
|
|
@@ -2251,7 +2320,7 @@ var SendoraSDK = class {
|
|
|
2251
2320
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before support.createTicket().");
|
|
2252
2321
|
const res = await post(
|
|
2253
2322
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2254
|
-
"/support/tickets",
|
|
2323
|
+
"/api/v1/support/tickets",
|
|
2255
2324
|
input
|
|
2256
2325
|
);
|
|
2257
2326
|
if (!res || !res.ok) throw new Error(`[sendoracloud] support.createTicket failed (${res?.status ?? "network"})`);
|
|
@@ -2263,7 +2332,7 @@ var SendoraSDK = class {
|
|
|
2263
2332
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before support.submitCsat().");
|
|
2264
2333
|
const res = await post(
|
|
2265
2334
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2266
|
-
"/support/csat",
|
|
2335
|
+
"/api/v1/support/csat",
|
|
2267
2336
|
input
|
|
2268
2337
|
);
|
|
2269
2338
|
if (!res || !res.ok) throw new Error(`[sendoracloud] support.submitCsat failed (${res?.status ?? "network"})`);
|
|
@@ -2352,7 +2421,7 @@ var SendoraSDK = class {
|
|
|
2352
2421
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before consent.record().");
|
|
2353
2422
|
const res = await post(
|
|
2354
2423
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2355
|
-
"/consent/record",
|
|
2424
|
+
"/api/v1/consent/record",
|
|
2356
2425
|
input
|
|
2357
2426
|
);
|
|
2358
2427
|
if (!res || !res.ok) throw new Error(`[sendoracloud] consent.record failed (${res?.status ?? "network"})`);
|
|
@@ -2364,7 +2433,7 @@ var SendoraSDK = class {
|
|
|
2364
2433
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before consent.requestDeletion().");
|
|
2365
2434
|
const res = await post(
|
|
2366
2435
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2367
|
-
"/consent/deletion-request",
|
|
2436
|
+
"/api/v1/consent/deletion-request",
|
|
2368
2437
|
input
|
|
2369
2438
|
);
|
|
2370
2439
|
if (!res || !res.ok) throw new Error(`[sendoracloud] consent.requestDeletion failed (${res?.status ?? "network"})`);
|
|
@@ -2415,7 +2484,7 @@ var SendoraSDK = class {
|
|
|
2415
2484
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before surveys.submitResponse().");
|
|
2416
2485
|
const res = await post(
|
|
2417
2486
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2418
|
-
"/surveys/responses",
|
|
2487
|
+
"/api/v1/surveys/responses",
|
|
2419
2488
|
{ completed: true, ...input }
|
|
2420
2489
|
);
|
|
2421
2490
|
if (!res || !res.ok) throw new Error(`[sendoracloud] surveys.submitResponse failed (${res?.status ?? "network"})`);
|
|
@@ -2465,7 +2534,7 @@ var SendoraSDK = class {
|
|
|
2465
2534
|
if (!self.config) throw new Error("[sendoracloud] init() must complete before messages.recordImpression().");
|
|
2466
2535
|
const res = await post(
|
|
2467
2536
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2468
|
-
"/in-app-messages/impressions",
|
|
2537
|
+
"/api/v1/in-app-messages/impressions",
|
|
2469
2538
|
input
|
|
2470
2539
|
);
|
|
2471
2540
|
if (!res || !res.ok) throw new Error(`[sendoracloud] messages.recordImpression failed (${res?.status ?? "network"})`);
|
|
@@ -2503,7 +2572,7 @@ var SendoraSDK = class {
|
|
|
2503
2572
|
};
|
|
2504
2573
|
const res = await post(
|
|
2505
2574
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2506
|
-
"/feature-flags/evaluate",
|
|
2575
|
+
"/api/v1/feature-flags/evaluate",
|
|
2507
2576
|
body
|
|
2508
2577
|
);
|
|
2509
2578
|
if (!res || !res.ok) throw new Error(`[sendoracloud] flags.evaluate failed (${res?.status ?? "network"})`);
|
|
@@ -2520,7 +2589,7 @@ var SendoraSDK = class {
|
|
|
2520
2589
|
};
|
|
2521
2590
|
const res = await post(
|
|
2522
2591
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2523
|
-
"/feature-flags/evaluate-all",
|
|
2592
|
+
"/api/v1/feature-flags/evaluate-all",
|
|
2524
2593
|
body
|
|
2525
2594
|
);
|
|
2526
2595
|
if (!res || !res.ok) throw new Error(`[sendoracloud] flags.evaluateAll failed (${res?.status ?? "network"})`);
|
|
@@ -2555,7 +2624,7 @@ var SendoraSDK = class {
|
|
|
2555
2624
|
};
|
|
2556
2625
|
const res = await post(
|
|
2557
2626
|
{ apiUrl: self.config.apiUrl, publicKey: self.config.publicKey, debug: self.debug },
|
|
2558
|
-
"/chatbot/message",
|
|
2627
|
+
"/api/v1/chatbot/message",
|
|
2559
2628
|
body
|
|
2560
2629
|
);
|
|
2561
2630
|
if (!res || !res.ok) throw new Error(`[sendoracloud] chatbot.sendMessage failed (${res?.status ?? "network"})`);
|
|
@@ -2583,6 +2652,7 @@ var SendoraSDK = class {
|
|
|
2583
2652
|
}
|
|
2584
2653
|
/** Tear down auto-track listeners. Call in test harnesses. */
|
|
2585
2654
|
destroy() {
|
|
2655
|
+
this.engFlush();
|
|
2586
2656
|
if (this.autoTrackCleanup) {
|
|
2587
2657
|
this.autoTrackCleanup();
|
|
2588
2658
|
this.autoTrackCleanup = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendoracloud/sdk-react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Sendora Cloud React Native + Expo SDK — analytics, identity, push-token registration, auth, deep links (Branch / Firebase Dynamic Links parity). Auth + analytics + deep links work in Expo Go; push token registration requires a Dev Client (EAS Build or `npx expo prebuild`).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|