@akhilpulse/samadhaan-session-replay 0.3.4 → 0.3.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/package.json +1 -1
- package/src/SessionReplayProvider.tsx +73 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akhilpulse/samadhaan-session-replay",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "React Native SDK for Samadhaan Guided Support — session recording, live remote screen-share, take-control, and guided-tour overlays.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -47,7 +47,19 @@ const SessionReplayPixelCopy: PixelCopyNative | undefined =
|
|
|
47
47
|
let pixelCopyDisabled = false; // turned true after a runtime failure to avoid retrying every frame
|
|
48
48
|
|
|
49
49
|
const STORAGE_KEY = "@akhilpulse/samadhaan-session-replay/sessionId";
|
|
50
|
-
const SDK_VERSION = "0.3.
|
|
50
|
+
const SDK_VERSION = "0.3.6";
|
|
51
|
+
|
|
52
|
+
// ---- Module-level session singleton --------------------------------------
|
|
53
|
+
// Multiple SessionReplayProvider mounts (intentional or via React StrictMode's
|
|
54
|
+
// double-invoke in dev) used to each race to POST /sessions before any of
|
|
55
|
+
// them could persist the sessionId, producing 2-4 duplicate sessions on
|
|
56
|
+
// startup. We now share a single in-flight init promise across every
|
|
57
|
+
// Provider instance in the JS runtime so they all resolve to the same
|
|
58
|
+
// session.
|
|
59
|
+
type SessionInitResult = { sessionId: string; ingestToken: string; status: string } | null;
|
|
60
|
+
let _activeSession: SessionInitResult = null;
|
|
61
|
+
let _initPromise: Promise<SessionInitResult> | null = null;
|
|
62
|
+
let _providerMountCount = 0;
|
|
51
63
|
|
|
52
64
|
/** Default Samadhaan production endpoint. Override via `config.serverUrl` for staging. */
|
|
53
65
|
export const DEFAULT_SERVER_URL = "https://samadhaan.pulseenergy.io";
|
|
@@ -138,26 +150,47 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
138
150
|
// ---- Session init / resume ----
|
|
139
151
|
// initTick is bumped whenever we recycle (sessionId went to null at runtime) so this effect re-fires.
|
|
140
152
|
const [initTick, setInitTick] = useState(0);
|
|
153
|
+
|
|
154
|
+
// Mount-count singleton warning: if the host app accidentally renders the
|
|
155
|
+
// provider in two places, we still only init once (thanks to _initPromise),
|
|
156
|
+
// but we surface a warning so the developer knows.
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
_providerMountCount += 1;
|
|
159
|
+
if (_providerMountCount > 1 && __DEV__) {
|
|
160
|
+
console.warn(
|
|
161
|
+
`[session-replay] SessionReplayProvider is mounted ${_providerMountCount} times. ` +
|
|
162
|
+
`Render it exactly once at the root of your app — the SDK will reuse a single session, ` +
|
|
163
|
+
`but multiple mounts waste resources.`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return () => { _providerMountCount = Math.max(0, _providerMountCount - 1); };
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
141
169
|
useEffect(() => {
|
|
142
170
|
let cancelled = false;
|
|
143
|
-
|
|
171
|
+
|
|
172
|
+
// If another Provider instance (or a previous run of this same effect
|
|
173
|
+
// under StrictMode) already created a session, just adopt it. No new POST.
|
|
174
|
+
if (_activeSession) {
|
|
175
|
+
setSessionId(_activeSession.sessionId);
|
|
176
|
+
setIngestToken(_activeSession.ingestToken);
|
|
177
|
+
setStatus((_activeSession.status as Ctx["status"]) || "active");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const doInit = async (): Promise<SessionInitResult> => {
|
|
144
182
|
try {
|
|
145
183
|
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
146
|
-
if (stored
|
|
147
|
-
// resume via PUBLIC status endpoint (no auth needed) — also returns the ingest token
|
|
184
|
+
if (stored) {
|
|
148
185
|
const r = await fetch(`${serverUrl}/api/guided-support/sessions/${stored}/status`);
|
|
149
186
|
if (r.ok) {
|
|
150
187
|
const j = await r.json();
|
|
151
|
-
// If the stored session was already ended (server-side idle timeout while app
|
|
152
|
-
// was backgrounded), drop it and fall through to create a fresh one.
|
|
153
188
|
if (j.status === "ended") {
|
|
154
189
|
try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {}
|
|
155
190
|
} else {
|
|
156
|
-
|
|
157
|
-
return;
|
|
191
|
+
return { sessionId: stored, ingestToken: j.ingestToken, status: j.status || "active" };
|
|
158
192
|
}
|
|
159
193
|
} else {
|
|
160
|
-
// 404 / not found — clear stale id and create fresh.
|
|
161
194
|
try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {}
|
|
162
195
|
}
|
|
163
196
|
}
|
|
@@ -169,23 +202,41 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
169
202
|
user: config.user,
|
|
170
203
|
appVersion: config.appVersion,
|
|
171
204
|
sdkVersion: SDK_VERSION,
|
|
172
|
-
deviceInfo: {
|
|
173
|
-
os: Platform.OS,
|
|
174
|
-
osVersion: String(Platform.Version),
|
|
175
|
-
},
|
|
205
|
+
deviceInfo: { os: Platform.OS, osVersion: String(Platform.Version) },
|
|
176
206
|
}),
|
|
177
207
|
});
|
|
178
208
|
const j = await res.json();
|
|
179
|
-
if (
|
|
180
|
-
setSessionId(j.sessionId);
|
|
181
|
-
setIngestToken(j.ingestToken);
|
|
182
|
-
setStatus("active");
|
|
209
|
+
if (j.sessionId) {
|
|
183
210
|
await AsyncStorage.setItem(STORAGE_KEY, j.sessionId);
|
|
211
|
+
return { sessionId: j.sessionId, ingestToken: j.ingestToken, status: "active" };
|
|
184
212
|
}
|
|
185
|
-
|
|
186
|
-
|
|
213
|
+
return null;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
187
216
|
}
|
|
188
|
-
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Coalesce concurrent inits into a single in-flight promise. Any later
|
|
220
|
+
// mount that arrives while this is pending will await the same promise
|
|
221
|
+
// instead of issuing its own POST.
|
|
222
|
+
if (!_initPromise) {
|
|
223
|
+
_initPromise = doInit().then((result) => {
|
|
224
|
+
_activeSession = result;
|
|
225
|
+
return result;
|
|
226
|
+
}).finally(() => {
|
|
227
|
+
// Clear the promise once settled so a later recycle (initTick bump
|
|
228
|
+
// after the server idle-ends the session) can re-init.
|
|
229
|
+
_initPromise = null;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_initPromise.then((result) => {
|
|
234
|
+
if (cancelled || !result) return;
|
|
235
|
+
setSessionId(result.sessionId);
|
|
236
|
+
setIngestToken(result.ingestToken);
|
|
237
|
+
setStatus((result.status as Ctx["status"]) || "active");
|
|
238
|
+
});
|
|
239
|
+
|
|
189
240
|
return () => { cancelled = true; };
|
|
190
241
|
}, [config.appId, serverUrl, config.appVersion, config.user, initTick]);
|
|
191
242
|
|
|
@@ -618,10 +669,10 @@ const styles = StyleSheet.create({
|
|
|
618
669
|
consentTitle: { color: "#0f172a", fontSize: 18, fontWeight: "700", marginBottom: 8, textAlign: "center" },
|
|
619
670
|
consentBody: { color: "#475569", fontSize: 14, lineHeight: 20, textAlign: "center", marginBottom: 20 },
|
|
620
671
|
consentEmphasis: { color: "#0f172a", fontWeight: "600" },
|
|
621
|
-
consentRow: { flexDirection: "row", justifyContent: "center",
|
|
672
|
+
consentRow: { flexDirection: "row", justifyContent: "center", width: "100%", marginHorizontal: -5 },
|
|
622
673
|
btn: {
|
|
623
674
|
flex: 1, paddingVertical: 12, paddingHorizontal: 16, borderRadius: 10,
|
|
624
|
-
alignItems: "center", justifyContent: "center", minHeight: 44,
|
|
675
|
+
alignItems: "center", justifyContent: "center", minHeight: 44, marginHorizontal: 5,
|
|
625
676
|
},
|
|
626
677
|
btnPressed: { opacity: 0.7 },
|
|
627
678
|
btnGhost: { backgroundColor: "#f1f5f9" },
|