@founderhq/journeys 0.4.1 → 0.4.2
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/README.md +6 -0
- package/dist/index.cjs +177 -70
- package/dist/index.js +177 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -208,6 +208,12 @@ Capture is enabled by default after access validation succeeds. Pass `capture={f
|
|
|
208
208
|
|
|
209
209
|
`apiKey`, `journeyId`, and the FounderHQ API URL are inherited from the root props and are not repeated inside `capture`.
|
|
210
210
|
|
|
211
|
+
Capture events are batched and persisted locally with a TTL. The runtime flushes
|
|
212
|
+
immediately for completion events, and browser embeds also try an exit-safe
|
|
213
|
+
`sendBeacon`/`keepalive` flush on `visibilitychange` and `pagehide`. Beacon
|
|
214
|
+
delivery is treated as best-effort; FounderHQ uses event IDs server-side so a
|
|
215
|
+
later retry can safely duplicate a queued event.
|
|
216
|
+
|
|
211
217
|
## Config Shape
|
|
212
218
|
|
|
213
219
|
A journey config is centered around:
|
package/dist/index.cjs
CHANGED
|
@@ -9693,20 +9693,15 @@ function JourneyShell({ className, theme } = {}) {
|
|
|
9693
9693
|
] });
|
|
9694
9694
|
}
|
|
9695
9695
|
var JOURNEY_LIBRARY_NAME = "@founderhq/journeys";
|
|
9696
|
-
var JOURNEY_LIBRARY_VERSION = "0.4.
|
|
9696
|
+
var JOURNEY_LIBRARY_VERSION = "0.4.2";
|
|
9697
|
+
var DEFAULT_QUEUE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
9698
|
+
var MAX_QUEUED_CAPTURE_EVENTS = 100;
|
|
9699
|
+
var MAX_BEACON_BYTES = 60 * 1024;
|
|
9697
9700
|
function randomId(prefix) {
|
|
9698
9701
|
const cryptoRef = globalThis.crypto;
|
|
9699
9702
|
if (cryptoRef == null ? void 0 : cryptoRef.randomUUID) return `${prefix}_${cryptoRef.randomUUID()}`;
|
|
9700
9703
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
9701
9704
|
}
|
|
9702
|
-
function readJson(storage, key, fallback) {
|
|
9703
|
-
try {
|
|
9704
|
-
const raw = storage.getItem(key);
|
|
9705
|
-
return raw ? JSON.parse(raw) : fallback;
|
|
9706
|
-
} catch (e) {
|
|
9707
|
-
return fallback;
|
|
9708
|
-
}
|
|
9709
|
-
}
|
|
9710
9705
|
function getStoredId(storage, key, prefix) {
|
|
9711
9706
|
try {
|
|
9712
9707
|
const existing = storage.getItem(key);
|
|
@@ -9718,6 +9713,64 @@ function getStoredId(storage, key, prefix) {
|
|
|
9718
9713
|
return randomId(prefix);
|
|
9719
9714
|
}
|
|
9720
9715
|
}
|
|
9716
|
+
function safeBrowserStorage(name) {
|
|
9717
|
+
var _a;
|
|
9718
|
+
if (typeof window === "undefined") return null;
|
|
9719
|
+
try {
|
|
9720
|
+
return (_a = window[name]) != null ? _a : null;
|
|
9721
|
+
} catch (e) {
|
|
9722
|
+
return null;
|
|
9723
|
+
}
|
|
9724
|
+
}
|
|
9725
|
+
function isQueuedCaptureEvent(value) {
|
|
9726
|
+
if (!isRecord(value)) return false;
|
|
9727
|
+
const event = value.event;
|
|
9728
|
+
return isRecord(event) && typeof event.id === "string" && typeof event.type === "string" && typeof event.occurredAt === "string" && typeof value.attempts === "number" && typeof value.createdAt === "number";
|
|
9729
|
+
}
|
|
9730
|
+
function pruneQueuedEvents(queue, now = Date.now(), ttlMs = DEFAULT_QUEUE_TTL_MS) {
|
|
9731
|
+
const cutoff = now - ttlMs;
|
|
9732
|
+
return queue.filter((item) => item.createdAt >= cutoff);
|
|
9733
|
+
}
|
|
9734
|
+
function readPersistedCaptureQueue(storage, key) {
|
|
9735
|
+
if (!storage) return null;
|
|
9736
|
+
try {
|
|
9737
|
+
const raw = storage.getItem(key);
|
|
9738
|
+
if (!raw) return null;
|
|
9739
|
+
const parsed = JSON.parse(raw);
|
|
9740
|
+
if (typeof parsed.visitorId !== "string" || typeof parsed.clientSessionId !== "string") {
|
|
9741
|
+
return null;
|
|
9742
|
+
}
|
|
9743
|
+
const queue = Array.isArray(parsed.queue) ? pruneQueuedEvents(parsed.queue.filter(isQueuedCaptureEvent)) : [];
|
|
9744
|
+
return {
|
|
9745
|
+
visitorId: parsed.visitorId,
|
|
9746
|
+
clientSessionId: parsed.clientSessionId,
|
|
9747
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
9748
|
+
queue
|
|
9749
|
+
};
|
|
9750
|
+
} catch (e) {
|
|
9751
|
+
return null;
|
|
9752
|
+
}
|
|
9753
|
+
}
|
|
9754
|
+
function persistCaptureQueue(storage, runtime, queue) {
|
|
9755
|
+
if (!storage) return;
|
|
9756
|
+
try {
|
|
9757
|
+
const pruned = pruneQueuedEvents(queue).slice(-MAX_QUEUED_CAPTURE_EVENTS);
|
|
9758
|
+
if (pruned.length === 0) {
|
|
9759
|
+
storage.removeItem(runtime.queueKey);
|
|
9760
|
+
return;
|
|
9761
|
+
}
|
|
9762
|
+
storage.setItem(
|
|
9763
|
+
runtime.queueKey,
|
|
9764
|
+
JSON.stringify({
|
|
9765
|
+
visitorId: runtime.visitorId,
|
|
9766
|
+
clientSessionId: runtime.clientSessionId,
|
|
9767
|
+
updatedAt: Date.now(),
|
|
9768
|
+
queue: pruned
|
|
9769
|
+
})
|
|
9770
|
+
);
|
|
9771
|
+
} catch (e) {
|
|
9772
|
+
}
|
|
9773
|
+
}
|
|
9721
9774
|
function isRecord(value) {
|
|
9722
9775
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9723
9776
|
}
|
|
@@ -9818,6 +9871,54 @@ function captureContext(capture, runtime) {
|
|
|
9818
9871
|
screen: screenRef ? { width: screenRef.width, height: screenRef.height } : void 0
|
|
9819
9872
|
}, customContext));
|
|
9820
9873
|
}
|
|
9874
|
+
function captureUrl(capture) {
|
|
9875
|
+
var _a;
|
|
9876
|
+
const baseUrl = (_a = capture.baseUrl) != null ? _a : "https://getfounderhq.com";
|
|
9877
|
+
return `${baseUrl}/api/v1/journeys/${encodeURIComponent(
|
|
9878
|
+
capture.journeyId
|
|
9879
|
+
)}/capture`;
|
|
9880
|
+
}
|
|
9881
|
+
function byteLength(value) {
|
|
9882
|
+
try {
|
|
9883
|
+
return new TextEncoder().encode(value).byteLength;
|
|
9884
|
+
} catch (e) {
|
|
9885
|
+
return value.length;
|
|
9886
|
+
}
|
|
9887
|
+
}
|
|
9888
|
+
function safeSendBeacon() {
|
|
9889
|
+
if (typeof navigator === "undefined") return null;
|
|
9890
|
+
const beacon = navigator.sendBeacon;
|
|
9891
|
+
return typeof beacon === "function" ? beacon.bind(navigator) : null;
|
|
9892
|
+
}
|
|
9893
|
+
async function sendCaptureBatch(capture, runtime, batch, options = {}) {
|
|
9894
|
+
const body = JSON.stringify(__spreadProps(__spreadValues({}, options.preferBeacon ? { apiKey: capture.apiKey } : {}), {
|
|
9895
|
+
clientSessionId: runtime.clientSessionId,
|
|
9896
|
+
visitorId: runtime.visitorId,
|
|
9897
|
+
context: captureContext(capture, runtime),
|
|
9898
|
+
events: batch.map((item) => item.event)
|
|
9899
|
+
}));
|
|
9900
|
+
const url = captureUrl(capture);
|
|
9901
|
+
if (options.preferBeacon && byteLength(body) <= MAX_BEACON_BYTES) {
|
|
9902
|
+
const beacon = safeSendBeacon();
|
|
9903
|
+
if (beacon == null ? void 0 : beacon(url, body)) {
|
|
9904
|
+
return "queued";
|
|
9905
|
+
}
|
|
9906
|
+
}
|
|
9907
|
+
try {
|
|
9908
|
+
const response = await fetch(url, {
|
|
9909
|
+
method: "POST",
|
|
9910
|
+
headers: {
|
|
9911
|
+
Authorization: `Bearer ${capture.apiKey}`,
|
|
9912
|
+
"Content-Type": "application/json"
|
|
9913
|
+
},
|
|
9914
|
+
body,
|
|
9915
|
+
keepalive: batch.length <= 5 || options.preferBeacon === true
|
|
9916
|
+
});
|
|
9917
|
+
return response.ok ? "acknowledged" : "failed";
|
|
9918
|
+
} catch (e) {
|
|
9919
|
+
return "failed";
|
|
9920
|
+
}
|
|
9921
|
+
}
|
|
9821
9922
|
function toJsonRecord(value) {
|
|
9822
9923
|
return isRecord(value) ? value : {};
|
|
9823
9924
|
}
|
|
@@ -9963,30 +10064,34 @@ function useJourneyCapture(params) {
|
|
|
9963
10064
|
captureRef.current = params.capture;
|
|
9964
10065
|
const persistQueue = React.useCallback(() => {
|
|
9965
10066
|
const runtime = runtimeRef.current;
|
|
9966
|
-
if (!runtime
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
} catch (e) {
|
|
9973
|
-
}
|
|
10067
|
+
if (!runtime) return;
|
|
10068
|
+
persistCaptureQueue(
|
|
10069
|
+
safeBrowserStorage("localStorage"),
|
|
10070
|
+
runtime,
|
|
10071
|
+
queueRef.current
|
|
10072
|
+
);
|
|
9974
10073
|
}, []);
|
|
9975
10074
|
const ensureRuntime = React.useCallback((capture) => {
|
|
10075
|
+
var _a, _b;
|
|
9976
10076
|
if (typeof window === "undefined") return null;
|
|
10077
|
+
const queueKey = `fhq_journey_queue:v2:${capture.apiKey.slice(
|
|
10078
|
+
0,
|
|
10079
|
+
16
|
|
10080
|
+
)}:${capture.journeyId}`;
|
|
9977
10081
|
const existing = runtimeRef.current;
|
|
9978
|
-
if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
);
|
|
9984
|
-
const
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
10082
|
+
if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId && existing.queueKey === queueKey) {
|
|
10083
|
+
return existing;
|
|
10084
|
+
}
|
|
10085
|
+
const local = safeBrowserStorage("localStorage");
|
|
10086
|
+
const session = safeBrowserStorage("sessionStorage");
|
|
10087
|
+
const persisted = readPersistedCaptureQueue(local, queueKey);
|
|
10088
|
+
const visitorId = (_a = persisted == null ? void 0 : persisted.visitorId) != null ? _a : local ? getStoredId(local, "fhq_journey_visitor_id", "fhqv") : randomId("fhqv");
|
|
10089
|
+
const sessionKey = `fhq_journey_session:${capture.journeyId}`;
|
|
10090
|
+
const clientSessionId = persisted && persisted.queue.length > 0 ? persisted.clientSessionId : session ? getStoredId(session, sessionKey, "fhqs") : randomId("fhqs");
|
|
10091
|
+
try {
|
|
10092
|
+
session == null ? void 0 : session.setItem(sessionKey, clientSessionId);
|
|
10093
|
+
} catch (e) {
|
|
10094
|
+
}
|
|
9990
10095
|
const runtime = {
|
|
9991
10096
|
journeyId: capture.journeyId,
|
|
9992
10097
|
visitorId,
|
|
@@ -9994,51 +10099,46 @@ function useJourneyCapture(params) {
|
|
|
9994
10099
|
queueKey
|
|
9995
10100
|
};
|
|
9996
10101
|
runtimeRef.current = runtime;
|
|
9997
|
-
queueRef.current =
|
|
9998
|
-
sessionStorage,
|
|
9999
|
-
queueKey,
|
|
10000
|
-
[]
|
|
10001
|
-
);
|
|
10102
|
+
queueRef.current = (_b = persisted == null ? void 0 : persisted.queue) != null ? _b : [];
|
|
10002
10103
|
return runtime;
|
|
10003
10104
|
}, []);
|
|
10004
|
-
const flush = React.useCallback(async () => {
|
|
10005
|
-
var _a, _b
|
|
10105
|
+
const flush = React.useCallback(async (options = {}) => {
|
|
10106
|
+
var _a, _b;
|
|
10006
10107
|
const capture = captureRef.current;
|
|
10007
|
-
if (!capture || !configRef.current
|
|
10108
|
+
if (!capture || !configRef.current) return;
|
|
10109
|
+
if (!options.preferBeacon && flushingRef.current) return;
|
|
10008
10110
|
const runtime = ensureRuntime(capture);
|
|
10009
10111
|
if (!runtime || queueRef.current.length === 0) return;
|
|
10010
|
-
|
|
10112
|
+
queueRef.current = pruneQueuedEvents(queueRef.current);
|
|
10011
10113
|
const batchSize = Math.max(1, (_a = capture.batchSize) != null ? _a : 10);
|
|
10012
10114
|
const maxRetries = Math.max(1, (_b = capture.maxRetries) != null ? _b : 3);
|
|
10013
|
-
const batch = queueRef.current.slice(
|
|
10014
|
-
|
|
10115
|
+
const batch = queueRef.current.slice(
|
|
10116
|
+
0,
|
|
10117
|
+
options.preferBeacon ? Math.min(batchSize, 5) : batchSize
|
|
10118
|
+
);
|
|
10119
|
+
if (batch.length === 0) {
|
|
10120
|
+
persistQueue();
|
|
10121
|
+
return;
|
|
10122
|
+
}
|
|
10123
|
+
if (options.preferBeacon) {
|
|
10124
|
+
await sendCaptureBatch(capture, runtime, batch, options);
|
|
10125
|
+
persistQueue();
|
|
10126
|
+
return;
|
|
10127
|
+
}
|
|
10128
|
+
flushingRef.current = true;
|
|
10015
10129
|
try {
|
|
10016
|
-
const
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
},
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
visitorId: runtime.visitorId,
|
|
10029
|
-
context: captureContext(capture, runtime),
|
|
10030
|
-
events: batch.map((item) => item.event)
|
|
10031
|
-
}),
|
|
10032
|
-
keepalive: batch.length <= 5
|
|
10033
|
-
}
|
|
10034
|
-
);
|
|
10035
|
-
if (!response.ok) throw new Error(`Capture failed: ${response.status}`);
|
|
10036
|
-
queueRef.current = queueRef.current.slice(batch.length);
|
|
10037
|
-
} catch (e) {
|
|
10038
|
-
const failedIds = new Set(batch.map((item) => item.event.id));
|
|
10039
|
-
queueRef.current = queueRef.current.map(
|
|
10040
|
-
(item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
|
|
10041
|
-
).filter((item) => item.attempts < maxRetries);
|
|
10130
|
+
const result = await sendCaptureBatch(capture, runtime, batch);
|
|
10131
|
+
if (result === "acknowledged") {
|
|
10132
|
+
const sentIds = new Set(batch.map((item) => item.event.id));
|
|
10133
|
+
queueRef.current = queueRef.current.filter(
|
|
10134
|
+
(item) => !sentIds.has(item.event.id)
|
|
10135
|
+
);
|
|
10136
|
+
} else {
|
|
10137
|
+
const failedIds = new Set(batch.map((item) => item.event.id));
|
|
10138
|
+
queueRef.current = queueRef.current.map(
|
|
10139
|
+
(item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
|
|
10140
|
+
).filter((item) => item.attempts < maxRetries);
|
|
10141
|
+
}
|
|
10042
10142
|
} finally {
|
|
10043
10143
|
persistQueue();
|
|
10044
10144
|
flushingRef.current = false;
|
|
@@ -10054,15 +10154,18 @@ function useJourneyCapture(params) {
|
|
|
10054
10154
|
Math.max(1e3, (_a = capture.flushIntervalMs) != null ? _a : 3e3)
|
|
10055
10155
|
);
|
|
10056
10156
|
const handleVisibility = () => {
|
|
10057
|
-
if (document.visibilityState === "hidden")
|
|
10157
|
+
if (document.visibilityState === "hidden") {
|
|
10158
|
+
void flush({ preferBeacon: true });
|
|
10159
|
+
}
|
|
10058
10160
|
};
|
|
10059
|
-
|
|
10161
|
+
const handlePageHide = () => void flush({ preferBeacon: true });
|
|
10060
10162
|
document.addEventListener("visibilitychange", handleVisibility);
|
|
10163
|
+
window.addEventListener("pagehide", handlePageHide);
|
|
10061
10164
|
void flush();
|
|
10062
10165
|
return () => {
|
|
10063
10166
|
window.clearInterval(interval);
|
|
10064
|
-
window.removeEventListener("beforeunload", persistQueue);
|
|
10065
10167
|
document.removeEventListener("visibilitychange", handleVisibility);
|
|
10168
|
+
window.removeEventListener("pagehide", handlePageHide);
|
|
10066
10169
|
persistQueue();
|
|
10067
10170
|
};
|
|
10068
10171
|
}, [ensureRuntime, flush, persistQueue, params.capture]);
|
|
@@ -10078,8 +10181,12 @@ function useJourneyCapture(params) {
|
|
|
10078
10181
|
sequenceRef.current += 1;
|
|
10079
10182
|
queueRef.current.push({
|
|
10080
10183
|
event: toCaptureEvent(config, event, sequenceRef.current, runtime),
|
|
10081
|
-
attempts: 0
|
|
10184
|
+
attempts: 0,
|
|
10185
|
+
createdAt: Date.now()
|
|
10082
10186
|
});
|
|
10187
|
+
queueRef.current = pruneQueuedEvents(queueRef.current).slice(
|
|
10188
|
+
-MAX_QUEUED_CAPTURE_EVENTS
|
|
10189
|
+
);
|
|
10083
10190
|
persistQueue();
|
|
10084
10191
|
const batchSize = Math.max(1, (_b = capture.batchSize) != null ? _b : 10);
|
|
10085
10192
|
if (event.type === "complete" || queueRef.current.length >= batchSize) {
|
package/dist/index.js
CHANGED
|
@@ -9668,20 +9668,15 @@ function JourneyShell({ className, theme } = {}) {
|
|
|
9668
9668
|
] });
|
|
9669
9669
|
}
|
|
9670
9670
|
var JOURNEY_LIBRARY_NAME = "@founderhq/journeys";
|
|
9671
|
-
var JOURNEY_LIBRARY_VERSION = "0.4.
|
|
9671
|
+
var JOURNEY_LIBRARY_VERSION = "0.4.2";
|
|
9672
|
+
var DEFAULT_QUEUE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
9673
|
+
var MAX_QUEUED_CAPTURE_EVENTS = 100;
|
|
9674
|
+
var MAX_BEACON_BYTES = 60 * 1024;
|
|
9672
9675
|
function randomId(prefix) {
|
|
9673
9676
|
const cryptoRef = globalThis.crypto;
|
|
9674
9677
|
if (cryptoRef == null ? void 0 : cryptoRef.randomUUID) return `${prefix}_${cryptoRef.randomUUID()}`;
|
|
9675
9678
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
|
9676
9679
|
}
|
|
9677
|
-
function readJson(storage, key, fallback) {
|
|
9678
|
-
try {
|
|
9679
|
-
const raw = storage.getItem(key);
|
|
9680
|
-
return raw ? JSON.parse(raw) : fallback;
|
|
9681
|
-
} catch (e) {
|
|
9682
|
-
return fallback;
|
|
9683
|
-
}
|
|
9684
|
-
}
|
|
9685
9680
|
function getStoredId(storage, key, prefix) {
|
|
9686
9681
|
try {
|
|
9687
9682
|
const existing = storage.getItem(key);
|
|
@@ -9693,6 +9688,64 @@ function getStoredId(storage, key, prefix) {
|
|
|
9693
9688
|
return randomId(prefix);
|
|
9694
9689
|
}
|
|
9695
9690
|
}
|
|
9691
|
+
function safeBrowserStorage(name) {
|
|
9692
|
+
var _a;
|
|
9693
|
+
if (typeof window === "undefined") return null;
|
|
9694
|
+
try {
|
|
9695
|
+
return (_a = window[name]) != null ? _a : null;
|
|
9696
|
+
} catch (e) {
|
|
9697
|
+
return null;
|
|
9698
|
+
}
|
|
9699
|
+
}
|
|
9700
|
+
function isQueuedCaptureEvent(value) {
|
|
9701
|
+
if (!isRecord(value)) return false;
|
|
9702
|
+
const event = value.event;
|
|
9703
|
+
return isRecord(event) && typeof event.id === "string" && typeof event.type === "string" && typeof event.occurredAt === "string" && typeof value.attempts === "number" && typeof value.createdAt === "number";
|
|
9704
|
+
}
|
|
9705
|
+
function pruneQueuedEvents(queue, now = Date.now(), ttlMs = DEFAULT_QUEUE_TTL_MS) {
|
|
9706
|
+
const cutoff = now - ttlMs;
|
|
9707
|
+
return queue.filter((item) => item.createdAt >= cutoff);
|
|
9708
|
+
}
|
|
9709
|
+
function readPersistedCaptureQueue(storage, key) {
|
|
9710
|
+
if (!storage) return null;
|
|
9711
|
+
try {
|
|
9712
|
+
const raw = storage.getItem(key);
|
|
9713
|
+
if (!raw) return null;
|
|
9714
|
+
const parsed = JSON.parse(raw);
|
|
9715
|
+
if (typeof parsed.visitorId !== "string" || typeof parsed.clientSessionId !== "string") {
|
|
9716
|
+
return null;
|
|
9717
|
+
}
|
|
9718
|
+
const queue = Array.isArray(parsed.queue) ? pruneQueuedEvents(parsed.queue.filter(isQueuedCaptureEvent)) : [];
|
|
9719
|
+
return {
|
|
9720
|
+
visitorId: parsed.visitorId,
|
|
9721
|
+
clientSessionId: parsed.clientSessionId,
|
|
9722
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : Date.now(),
|
|
9723
|
+
queue
|
|
9724
|
+
};
|
|
9725
|
+
} catch (e) {
|
|
9726
|
+
return null;
|
|
9727
|
+
}
|
|
9728
|
+
}
|
|
9729
|
+
function persistCaptureQueue(storage, runtime, queue) {
|
|
9730
|
+
if (!storage) return;
|
|
9731
|
+
try {
|
|
9732
|
+
const pruned = pruneQueuedEvents(queue).slice(-MAX_QUEUED_CAPTURE_EVENTS);
|
|
9733
|
+
if (pruned.length === 0) {
|
|
9734
|
+
storage.removeItem(runtime.queueKey);
|
|
9735
|
+
return;
|
|
9736
|
+
}
|
|
9737
|
+
storage.setItem(
|
|
9738
|
+
runtime.queueKey,
|
|
9739
|
+
JSON.stringify({
|
|
9740
|
+
visitorId: runtime.visitorId,
|
|
9741
|
+
clientSessionId: runtime.clientSessionId,
|
|
9742
|
+
updatedAt: Date.now(),
|
|
9743
|
+
queue: pruned
|
|
9744
|
+
})
|
|
9745
|
+
);
|
|
9746
|
+
} catch (e) {
|
|
9747
|
+
}
|
|
9748
|
+
}
|
|
9696
9749
|
function isRecord(value) {
|
|
9697
9750
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9698
9751
|
}
|
|
@@ -9793,6 +9846,54 @@ function captureContext(capture, runtime) {
|
|
|
9793
9846
|
screen: screenRef ? { width: screenRef.width, height: screenRef.height } : void 0
|
|
9794
9847
|
}, customContext));
|
|
9795
9848
|
}
|
|
9849
|
+
function captureUrl(capture) {
|
|
9850
|
+
var _a;
|
|
9851
|
+
const baseUrl = (_a = capture.baseUrl) != null ? _a : "https://getfounderhq.com";
|
|
9852
|
+
return `${baseUrl}/api/v1/journeys/${encodeURIComponent(
|
|
9853
|
+
capture.journeyId
|
|
9854
|
+
)}/capture`;
|
|
9855
|
+
}
|
|
9856
|
+
function byteLength(value) {
|
|
9857
|
+
try {
|
|
9858
|
+
return new TextEncoder().encode(value).byteLength;
|
|
9859
|
+
} catch (e) {
|
|
9860
|
+
return value.length;
|
|
9861
|
+
}
|
|
9862
|
+
}
|
|
9863
|
+
function safeSendBeacon() {
|
|
9864
|
+
if (typeof navigator === "undefined") return null;
|
|
9865
|
+
const beacon = navigator.sendBeacon;
|
|
9866
|
+
return typeof beacon === "function" ? beacon.bind(navigator) : null;
|
|
9867
|
+
}
|
|
9868
|
+
async function sendCaptureBatch(capture, runtime, batch, options = {}) {
|
|
9869
|
+
const body = JSON.stringify(__spreadProps(__spreadValues({}, options.preferBeacon ? { apiKey: capture.apiKey } : {}), {
|
|
9870
|
+
clientSessionId: runtime.clientSessionId,
|
|
9871
|
+
visitorId: runtime.visitorId,
|
|
9872
|
+
context: captureContext(capture, runtime),
|
|
9873
|
+
events: batch.map((item) => item.event)
|
|
9874
|
+
}));
|
|
9875
|
+
const url = captureUrl(capture);
|
|
9876
|
+
if (options.preferBeacon && byteLength(body) <= MAX_BEACON_BYTES) {
|
|
9877
|
+
const beacon = safeSendBeacon();
|
|
9878
|
+
if (beacon == null ? void 0 : beacon(url, body)) {
|
|
9879
|
+
return "queued";
|
|
9880
|
+
}
|
|
9881
|
+
}
|
|
9882
|
+
try {
|
|
9883
|
+
const response = await fetch(url, {
|
|
9884
|
+
method: "POST",
|
|
9885
|
+
headers: {
|
|
9886
|
+
Authorization: `Bearer ${capture.apiKey}`,
|
|
9887
|
+
"Content-Type": "application/json"
|
|
9888
|
+
},
|
|
9889
|
+
body,
|
|
9890
|
+
keepalive: batch.length <= 5 || options.preferBeacon === true
|
|
9891
|
+
});
|
|
9892
|
+
return response.ok ? "acknowledged" : "failed";
|
|
9893
|
+
} catch (e) {
|
|
9894
|
+
return "failed";
|
|
9895
|
+
}
|
|
9896
|
+
}
|
|
9796
9897
|
function toJsonRecord(value) {
|
|
9797
9898
|
return isRecord(value) ? value : {};
|
|
9798
9899
|
}
|
|
@@ -9938,30 +10039,34 @@ function useJourneyCapture(params) {
|
|
|
9938
10039
|
captureRef.current = params.capture;
|
|
9939
10040
|
const persistQueue = useCallback(() => {
|
|
9940
10041
|
const runtime = runtimeRef.current;
|
|
9941
|
-
if (!runtime
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
} catch (e) {
|
|
9948
|
-
}
|
|
10042
|
+
if (!runtime) return;
|
|
10043
|
+
persistCaptureQueue(
|
|
10044
|
+
safeBrowserStorage("localStorage"),
|
|
10045
|
+
runtime,
|
|
10046
|
+
queueRef.current
|
|
10047
|
+
);
|
|
9949
10048
|
}, []);
|
|
9950
10049
|
const ensureRuntime = useCallback((capture) => {
|
|
10050
|
+
var _a, _b;
|
|
9951
10051
|
if (typeof window === "undefined") return null;
|
|
10052
|
+
const queueKey = `fhq_journey_queue:v2:${capture.apiKey.slice(
|
|
10053
|
+
0,
|
|
10054
|
+
16
|
|
10055
|
+
)}:${capture.journeyId}`;
|
|
9952
10056
|
const existing = runtimeRef.current;
|
|
9953
|
-
if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
);
|
|
9959
|
-
const
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
10057
|
+
if ((existing == null ? void 0 : existing.journeyId) === capture.journeyId && existing.queueKey === queueKey) {
|
|
10058
|
+
return existing;
|
|
10059
|
+
}
|
|
10060
|
+
const local = safeBrowserStorage("localStorage");
|
|
10061
|
+
const session = safeBrowserStorage("sessionStorage");
|
|
10062
|
+
const persisted = readPersistedCaptureQueue(local, queueKey);
|
|
10063
|
+
const visitorId = (_a = persisted == null ? void 0 : persisted.visitorId) != null ? _a : local ? getStoredId(local, "fhq_journey_visitor_id", "fhqv") : randomId("fhqv");
|
|
10064
|
+
const sessionKey = `fhq_journey_session:${capture.journeyId}`;
|
|
10065
|
+
const clientSessionId = persisted && persisted.queue.length > 0 ? persisted.clientSessionId : session ? getStoredId(session, sessionKey, "fhqs") : randomId("fhqs");
|
|
10066
|
+
try {
|
|
10067
|
+
session == null ? void 0 : session.setItem(sessionKey, clientSessionId);
|
|
10068
|
+
} catch (e) {
|
|
10069
|
+
}
|
|
9965
10070
|
const runtime = {
|
|
9966
10071
|
journeyId: capture.journeyId,
|
|
9967
10072
|
visitorId,
|
|
@@ -9969,51 +10074,46 @@ function useJourneyCapture(params) {
|
|
|
9969
10074
|
queueKey
|
|
9970
10075
|
};
|
|
9971
10076
|
runtimeRef.current = runtime;
|
|
9972
|
-
queueRef.current =
|
|
9973
|
-
sessionStorage,
|
|
9974
|
-
queueKey,
|
|
9975
|
-
[]
|
|
9976
|
-
);
|
|
10077
|
+
queueRef.current = (_b = persisted == null ? void 0 : persisted.queue) != null ? _b : [];
|
|
9977
10078
|
return runtime;
|
|
9978
10079
|
}, []);
|
|
9979
|
-
const flush = useCallback(async () => {
|
|
9980
|
-
var _a, _b
|
|
10080
|
+
const flush = useCallback(async (options = {}) => {
|
|
10081
|
+
var _a, _b;
|
|
9981
10082
|
const capture = captureRef.current;
|
|
9982
|
-
if (!capture || !configRef.current
|
|
10083
|
+
if (!capture || !configRef.current) return;
|
|
10084
|
+
if (!options.preferBeacon && flushingRef.current) return;
|
|
9983
10085
|
const runtime = ensureRuntime(capture);
|
|
9984
10086
|
if (!runtime || queueRef.current.length === 0) return;
|
|
9985
|
-
|
|
10087
|
+
queueRef.current = pruneQueuedEvents(queueRef.current);
|
|
9986
10088
|
const batchSize = Math.max(1, (_a = capture.batchSize) != null ? _a : 10);
|
|
9987
10089
|
const maxRetries = Math.max(1, (_b = capture.maxRetries) != null ? _b : 3);
|
|
9988
|
-
const batch = queueRef.current.slice(
|
|
9989
|
-
|
|
10090
|
+
const batch = queueRef.current.slice(
|
|
10091
|
+
0,
|
|
10092
|
+
options.preferBeacon ? Math.min(batchSize, 5) : batchSize
|
|
10093
|
+
);
|
|
10094
|
+
if (batch.length === 0) {
|
|
10095
|
+
persistQueue();
|
|
10096
|
+
return;
|
|
10097
|
+
}
|
|
10098
|
+
if (options.preferBeacon) {
|
|
10099
|
+
await sendCaptureBatch(capture, runtime, batch, options);
|
|
10100
|
+
persistQueue();
|
|
10101
|
+
return;
|
|
10102
|
+
}
|
|
10103
|
+
flushingRef.current = true;
|
|
9990
10104
|
try {
|
|
9991
|
-
const
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
},
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
visitorId: runtime.visitorId,
|
|
10004
|
-
context: captureContext(capture, runtime),
|
|
10005
|
-
events: batch.map((item) => item.event)
|
|
10006
|
-
}),
|
|
10007
|
-
keepalive: batch.length <= 5
|
|
10008
|
-
}
|
|
10009
|
-
);
|
|
10010
|
-
if (!response.ok) throw new Error(`Capture failed: ${response.status}`);
|
|
10011
|
-
queueRef.current = queueRef.current.slice(batch.length);
|
|
10012
|
-
} catch (e) {
|
|
10013
|
-
const failedIds = new Set(batch.map((item) => item.event.id));
|
|
10014
|
-
queueRef.current = queueRef.current.map(
|
|
10015
|
-
(item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
|
|
10016
|
-
).filter((item) => item.attempts < maxRetries);
|
|
10105
|
+
const result = await sendCaptureBatch(capture, runtime, batch);
|
|
10106
|
+
if (result === "acknowledged") {
|
|
10107
|
+
const sentIds = new Set(batch.map((item) => item.event.id));
|
|
10108
|
+
queueRef.current = queueRef.current.filter(
|
|
10109
|
+
(item) => !sentIds.has(item.event.id)
|
|
10110
|
+
);
|
|
10111
|
+
} else {
|
|
10112
|
+
const failedIds = new Set(batch.map((item) => item.event.id));
|
|
10113
|
+
queueRef.current = queueRef.current.map(
|
|
10114
|
+
(item) => failedIds.has(item.event.id) ? __spreadProps(__spreadValues({}, item), { attempts: item.attempts + 1 }) : item
|
|
10115
|
+
).filter((item) => item.attempts < maxRetries);
|
|
10116
|
+
}
|
|
10017
10117
|
} finally {
|
|
10018
10118
|
persistQueue();
|
|
10019
10119
|
flushingRef.current = false;
|
|
@@ -10029,15 +10129,18 @@ function useJourneyCapture(params) {
|
|
|
10029
10129
|
Math.max(1e3, (_a = capture.flushIntervalMs) != null ? _a : 3e3)
|
|
10030
10130
|
);
|
|
10031
10131
|
const handleVisibility = () => {
|
|
10032
|
-
if (document.visibilityState === "hidden")
|
|
10132
|
+
if (document.visibilityState === "hidden") {
|
|
10133
|
+
void flush({ preferBeacon: true });
|
|
10134
|
+
}
|
|
10033
10135
|
};
|
|
10034
|
-
|
|
10136
|
+
const handlePageHide = () => void flush({ preferBeacon: true });
|
|
10035
10137
|
document.addEventListener("visibilitychange", handleVisibility);
|
|
10138
|
+
window.addEventListener("pagehide", handlePageHide);
|
|
10036
10139
|
void flush();
|
|
10037
10140
|
return () => {
|
|
10038
10141
|
window.clearInterval(interval);
|
|
10039
|
-
window.removeEventListener("beforeunload", persistQueue);
|
|
10040
10142
|
document.removeEventListener("visibilitychange", handleVisibility);
|
|
10143
|
+
window.removeEventListener("pagehide", handlePageHide);
|
|
10041
10144
|
persistQueue();
|
|
10042
10145
|
};
|
|
10043
10146
|
}, [ensureRuntime, flush, persistQueue, params.capture]);
|
|
@@ -10053,8 +10156,12 @@ function useJourneyCapture(params) {
|
|
|
10053
10156
|
sequenceRef.current += 1;
|
|
10054
10157
|
queueRef.current.push({
|
|
10055
10158
|
event: toCaptureEvent(config, event, sequenceRef.current, runtime),
|
|
10056
|
-
attempts: 0
|
|
10159
|
+
attempts: 0,
|
|
10160
|
+
createdAt: Date.now()
|
|
10057
10161
|
});
|
|
10162
|
+
queueRef.current = pruneQueuedEvents(queueRef.current).slice(
|
|
10163
|
+
-MAX_QUEUED_CAPTURE_EVENTS
|
|
10164
|
+
);
|
|
10058
10165
|
persistQueue();
|
|
10059
10166
|
const batchSize = Math.max(1, (_b = capture.batchSize) != null ? _b : 10);
|
|
10060
10167
|
if (event.type === "complete" || queueRef.current.length >= batchSize) {
|